mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 02:05:41 +01:00
Cosign policy checker (#16187)
Enable policy checker for cosign, when it's enabled, user cannot pull artifact without cosign. Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
c0b496391c
commit
063991078a
@ -6450,6 +6450,10 @@ definitions:
|
||||
type: string
|
||||
description: 'Whether content trust is enabled or not. If it is enabled, user can''t pull unsigned images from this project. The valid values are "true", "false".'
|
||||
x-nullable: true
|
||||
enable_content_trust_cosign:
|
||||
type: string
|
||||
description: 'Whether cosign content trust is enabled or not. If it is enabled, user can''t pull images without cosign signature from this project. The valid values are "true", "false".'
|
||||
x-nullable: true
|
||||
prevent_vul:
|
||||
type: string
|
||||
description: 'Whether prevent the vulnerable images from running. The valid values are "true", "false".'
|
||||
|
@ -16,10 +16,11 @@ package models
|
||||
|
||||
// keys of project metadata and severity values
|
||||
const (
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist"
|
||||
ProMetaPublic = "public"
|
||||
ProMetaEnableContentTrust = "enable_content_trust"
|
||||
ProMetaEnableContentTrustCosign = "enable_content_trust_cosign"
|
||||
ProMetaPreventVul = "prevent_vul" // prevent vulnerable images from being pulled
|
||||
ProMetaSeverity = "severity"
|
||||
ProMetaAutoScan = "auto_scan"
|
||||
ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist"
|
||||
)
|
||||
|
@ -103,6 +103,15 @@ func (p *Project) ContentTrustEnabled() bool {
|
||||
return isTrue(enabled)
|
||||
}
|
||||
|
||||
// VulPrevented ...
|
||||
func (p *Project) ContentTrustCosignEnabled() bool {
|
||||
enabled, exist := p.GetMetadata(ProMetaEnableContentTrustCosign)
|
||||
if !exist {
|
||||
return false
|
||||
}
|
||||
return isTrue(enabled)
|
||||
}
|
||||
|
||||
// VulPrevented ...
|
||||
func (p *Project) VulPrevented() bool {
|
||||
prevent, exist := p.GetMetadata(ProMetaPreventVul)
|
||||
|
66
src/server/middleware/contenttrust/cosign.go
Normal file
66
src/server/middleware/contenttrust/cosign.go
Normal file
@ -0,0 +1,66 @@
|
||||
package contenttrust
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/goharbor/harbor/src/server/middleware/util"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Cosign handle docker pull content trust check
|
||||
func Cosign() func(http.Handler) http.Handler {
|
||||
return middleware.BeforeRequest(func(r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
logger := log.G(ctx)
|
||||
|
||||
none := lib.ArtifactInfo{}
|
||||
af := lib.GetArtifactInfo(ctx)
|
||||
if af == none {
|
||||
return errors.New("artifactinfo middleware required before this middleware").WithCode(errors.NotFoundCode)
|
||||
}
|
||||
pro, err := project.Ctl.GetByName(ctx, af.ProjectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if util.SkipPolicyChecking(r, pro.ProjectID) {
|
||||
logger.Debugf("artifact %s@%s is pulling by the scanner/cosign, skip the checking", af.Repository, af.Digest)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If cosign policy enabled, it has to at least have one cosign signature.
|
||||
if pro.ContentTrustCosignEnabled() {
|
||||
art, err := artifact.Ctl.GetByReference(ctx, af.Repository, af.Reference, &artifact.Option{
|
||||
WithAccessory: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(art.Accessories) == 0 {
|
||||
pkgE := errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed in Cosign.")
|
||||
return pkgE
|
||||
}
|
||||
|
||||
var hasCosignSignature bool
|
||||
for _, acc := range art.Accessories {
|
||||
if acc.GetData().Type == model.TypeCosignSignature {
|
||||
hasCosignSignature = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasCosignSignature {
|
||||
pkgE := errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed in Cosign.")
|
||||
return pkgE
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
209
src/server/middleware/contenttrust/cosign_test.go
Normal file
209
src/server/middleware/contenttrust/cosign_test.go
Normal file
@ -0,0 +1,209 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package contenttrust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/image"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
securitytesting "github.com/goharbor/harbor/src/testing/common/security"
|
||||
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CosignMiddlewareTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
originalArtifactController artifact.Controller
|
||||
artifactController *artifacttesting.Controller
|
||||
|
||||
originalProjectController project.Controller
|
||||
projectController *projecttesting.Controller
|
||||
|
||||
artifact *artifact.Artifact
|
||||
project *proModels.Project
|
||||
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) SetupTest() {
|
||||
suite.originalArtifactController = artifact.Ctl
|
||||
suite.artifactController = &artifacttesting.Controller{}
|
||||
artifact.Ctl = suite.artifactController
|
||||
|
||||
suite.originalProjectController = project.Ctl
|
||||
suite.projectController = &projecttesting.Controller{}
|
||||
project.Ctl = suite.projectController
|
||||
|
||||
suite.artifact = &artifact.Artifact{}
|
||||
suite.artifact.Type = image.ArtifactTypeImage
|
||||
suite.artifact.ProjectID = 1
|
||||
suite.artifact.RepositoryName = "library/photon"
|
||||
suite.artifact.Digest = "digest"
|
||||
|
||||
suite.project = &proModels.Project{
|
||||
ProjectID: suite.artifact.ProjectID,
|
||||
Name: "library",
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaEnableContentTrustCosign: "true",
|
||||
},
|
||||
}
|
||||
|
||||
suite.next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TearDownTest() {
|
||||
artifact.Ctl = suite.originalArtifactController
|
||||
project.Ctl = suite.originalProjectController
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) makeRequest(setHeader ...bool) *http.Request {
|
||||
req := httptest.NewRequest("GET", "/v1/library/photon/manifests/2.0", nil)
|
||||
info := lib.ArtifactInfo{
|
||||
Repository: "library/photon",
|
||||
Reference: "2.0",
|
||||
Tag: "2.0",
|
||||
Digest: "",
|
||||
}
|
||||
if len(setHeader) > 0 {
|
||||
if setHeader[0] {
|
||||
req.Header.Add("User-Agent", "cosign test")
|
||||
}
|
||||
}
|
||||
return req.WithContext(lib.WithArtifactInfo(req.Context(), info))
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestGetArtifactFailed() {
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(nil, fmt.Errorf("error"))
|
||||
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestGetProjectFailed() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(nil, fmt.Errorf("err"))
|
||||
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestContentTrustDisabled() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
suite.project.Metadata[proModels.ProMetaEnableContentTrustCosign] = "false"
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestNoneArtifact() {
|
||||
req := httptest.NewRequest("GET", "/v1/library/photon/manifests/nonexist", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestAuthenticatedUserPulling() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
securityCtx := &securitytesting.Context{}
|
||||
mock.OnAnything(securityCtx, "Name").Return("local")
|
||||
mock.OnAnything(securityCtx, "Can").Return(true, nil)
|
||||
mock.OnAnything(securityCtx, "IsAuthenticated").Return(true)
|
||||
|
||||
req := suite.makeRequest()
|
||||
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusPreconditionFailed)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestScannerPulling() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
securityCtx := &securitytesting.Context{}
|
||||
mock.OnAnything(securityCtx, "Name").Return("v2token")
|
||||
mock.OnAnything(securityCtx, "Can").Return(true, nil)
|
||||
mock.OnAnything(securityCtx, "IsAuthenticated").Return(true)
|
||||
|
||||
req := suite.makeRequest()
|
||||
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func (suite *CosignMiddlewareTestSuite) TestCosignPulling() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
securityCtx := &securitytesting.Context{}
|
||||
mock.OnAnything(securityCtx, "Name").Return("v2token")
|
||||
mock.OnAnything(securityCtx, "Can").Return(true, nil)
|
||||
mock.OnAnything(securityCtx, "IsAuthenticated").Return(true)
|
||||
|
||||
req := suite.makeRequest(true)
|
||||
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
// pull a public project a un-signed image when policy checker is enabled.
|
||||
func (suite *CosignMiddlewareTestSuite) TestUnAuthenticatedUserPulling() {
|
||||
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
|
||||
mock.OnAnything(suite.projectController, "GetByName").Return(suite.project, nil)
|
||||
securityCtx := &securitytesting.Context{}
|
||||
mock.OnAnything(securityCtx, "Name").Return("local")
|
||||
mock.OnAnything(securityCtx, "Can").Return(true, nil)
|
||||
mock.OnAnything(securityCtx, "IsAuthenticated").Return(false)
|
||||
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Cosign()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusPreconditionFailed)
|
||||
}
|
||||
|
||||
func TestCosignMiddlewareTestSuite(t *testing.T) {
|
||||
suite.Run(t, &CosignMiddlewareTestSuite{})
|
||||
}
|
@ -28,8 +28,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// Middleware handle docker pull content trust check
|
||||
func Middleware() func(http.Handler) http.Handler {
|
||||
// Notary handle docker pull content trust check
|
||||
func Notary() func(http.Handler) http.Handler {
|
||||
return middleware.BeforeRequest(func(r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
@ -52,7 +52,7 @@ func Middleware() func(http.Handler) http.Handler {
|
||||
return err
|
||||
}
|
||||
|
||||
if util.SkipPolicyChecking(ctx, pro.ProjectID) {
|
||||
if util.SkipPolicyChecking(r, pro.ProjectID) {
|
||||
// the artifact is pulling by the scanner, skip the checking
|
||||
logger.Debugf("artifact %s@%s is pulling by the scanner, skip the checking", af.Repository, af.Digest)
|
||||
return nil
|
@ -104,7 +104,7 @@ func (suite *MiddlewareTestSuite) TestGetArtifactFailed() {
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ func (suite *MiddlewareTestSuite) TestGetProjectFailed() {
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ func (suite *MiddlewareTestSuite) TestContentTrustDisabled() {
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ func (suite *MiddlewareTestSuite) TestNoneArtifact() {
|
||||
req := httptest.NewRequest("GET", "/v1/library/photon/manifests/nonexist", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ func (suite *MiddlewareTestSuite) TestAuthenticatedUserPulling() {
|
||||
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusPreconditionFailed)
|
||||
}
|
||||
|
||||
@ -167,7 +167,7 @@ func (suite *MiddlewareTestSuite) TestScannerPulling() {
|
||||
req = req.WithContext(security.NewContext(req.Context(), securityCtx))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
@ -183,7 +183,7 @@ func (suite *MiddlewareTestSuite) TestUnAuthenticatedUserPulling() {
|
||||
req := suite.makeRequest()
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
Middleware()(suite.next).ServeHTTP(rr, req)
|
||||
Notary()(suite.next).ServeHTTP(rr, req)
|
||||
suite.Equal(rr.Code, http.StatusPreconditionFailed)
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"net/http"
|
||||
@ -57,13 +56,17 @@ func ParseProjectName(r *http.Request) string {
|
||||
}
|
||||
|
||||
// SkipPolicyChecking ...
|
||||
func SkipPolicyChecking(ctx context.Context, projectID int64) bool {
|
||||
secCtx, ok := security.FromContext(ctx)
|
||||
func SkipPolicyChecking(r *http.Request, projectID int64) bool {
|
||||
secCtx, ok := security.FromContext(r.Context())
|
||||
|
||||
// only scanner pull access can bypass.
|
||||
if ok && secCtx.Name() == "v2token" &&
|
||||
secCtx.Can(ctx, rbac.ActionScannerPull, project.NewNamespace(projectID).Resource(rbac.ResourceRepository)) {
|
||||
return true
|
||||
// 1, scanner pull access can bypass.
|
||||
// 2, cosign pull can bypass, it needs to pull the manifest before pushing the signature.
|
||||
if ok && secCtx.Name() == "v2token" {
|
||||
if secCtx.Can(r.Context(), rbac.ActionScannerPull, project.NewNamespace(projectID).Resource(rbac.ResourceRepository)) ||
|
||||
(secCtx.Can(r.Context(), rbac.ActionPush, project.NewNamespace(projectID).Resource(rbac.ResourceRepository)) &&
|
||||
strings.Contains(r.UserAgent(), "cosign")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
@ -69,7 +69,7 @@ func Middleware() func(http.Handler) http.Handler {
|
||||
return nil
|
||||
}
|
||||
|
||||
if util.SkipPolicyChecking(ctx, proj.ProjectID) {
|
||||
if util.SkipPolicyChecking(r, proj.ProjectID) {
|
||||
// the artifact is pulling by the scanner, skip the checking
|
||||
logger.Debugf("artifact %s@%s is pulling by the scanner, skip the checking", art.RepositoryName, art.Digest)
|
||||
return nil
|
||||
|
@ -52,7 +52,8 @@ func RegisterRoutes() {
|
||||
Path("/*/manifests/:reference").
|
||||
Middleware(metric.InjectOpIDMiddleware(metric.ManifestOperationID)).
|
||||
Middleware(repoproxy.ManifestMiddleware()).
|
||||
Middleware(contenttrust.Middleware()).
|
||||
Middleware(contenttrust.Notary()).
|
||||
Middleware(contenttrust.Cosign()).
|
||||
Middleware(vulnerable.Middleware()).
|
||||
HandlerFunc(getManifest)
|
||||
root.NewRoute().
|
||||
@ -60,7 +61,8 @@ func RegisterRoutes() {
|
||||
Path("/*/manifests/:reference").
|
||||
Middleware(metric.InjectOpIDMiddleware(metric.ManifestOperationID)).
|
||||
Middleware(repoproxy.ManifestMiddleware()).
|
||||
Middleware(contenttrust.Middleware()).
|
||||
Middleware(contenttrust.Notary()).
|
||||
Middleware(contenttrust.Cosign()).
|
||||
Middleware(vulnerable.Middleware()).
|
||||
HandlerFunc(getManifest)
|
||||
root.NewRoute().
|
||||
|
@ -140,7 +140,7 @@ func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]strin
|
||||
}
|
||||
|
||||
switch key {
|
||||
case proModels.ProMetaPublic, proModels.ProMetaEnableContentTrust,
|
||||
case proModels.ProMetaPublic, proModels.ProMetaEnableContentTrust, proModels.ProMetaEnableContentTrustCosign,
|
||||
proModels.ProMetaPreventVul, proModels.ProMetaAutoScan:
|
||||
v, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user