mirror of https://github.com/goharbor/harbor.git
Merge branch 'main' into dependabot/npm_and_yarn/src/portal/multi-a1e856e0dc
This commit is contained in:
commit
c550259ef0
|
@ -41,10 +41,10 @@ jobs:
|
|||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -102,10 +102,10 @@ jobs:
|
|||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -157,10 +157,10 @@ jobs:
|
|||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -212,10 +212,10 @@ jobs:
|
|||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -265,10 +265,10 @@ jobs:
|
|||
- ubuntu-latest
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
|
@ -23,10 +23,10 @@ jobs:
|
|||
with:
|
||||
version: '430.0.0'
|
||||
- run: gcloud info
|
||||
- name: Set up Go 1.21
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- name: Setup Docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
|
|
@ -47,5 +47,8 @@ jobs:
|
|||
# make bootstrap
|
||||
# make release
|
||||
|
||||
# to make sure autobuild success, specifify golang version in go.mod
|
||||
# https://github.com/github/codeql/issues/15647#issuecomment-2003768106
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
- name: Set up Go 1.21
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.8
|
||||
go-version: 1.22.2
|
||||
id: go
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
|
@ -164,7 +164,8 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
|
|||
| 2.7 | 1.19.4 |
|
||||
| 2.8 | 1.20.6 |
|
||||
| 2.9 | 1.21.3 |
|
||||
| 2.10 | 1.21.8 |
|
||||
| 2.10 | 1.21.8 |
|
||||
| 2.11 | 1.22.2 |
|
||||
|
||||
|
||||
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.
|
||||
|
|
4
Makefile
4
Makefile
|
@ -140,7 +140,7 @@ GOINSTALL=$(GOCMD) install
|
|||
GOTEST=$(GOCMD) test
|
||||
GODEP=$(GOTEST) -i
|
||||
GOFMT=gofmt -w
|
||||
GOBUILDIMAGE=golang:1.21.8
|
||||
GOBUILDIMAGE=golang:1.22.2
|
||||
GOBUILDPATHINCONTAINER=/harbor
|
||||
|
||||
# go build
|
||||
|
@ -312,7 +312,7 @@ gen_apis: lint_apis
|
|||
|
||||
|
||||
MOCKERY_IMAGENAME=$(IMAGENAMESPACE)/mockery
|
||||
MOCKERY_VERSION=v2.35.4
|
||||
MOCKERY_VERSION=v2.42.2
|
||||
MOCKERY=$(RUNCONTAINER) ${MOCKERY_IMAGENAME}:${MOCKERY_VERSION}
|
||||
MOCKERY_IMAGE_BUILD_CMD=${DOCKERBUILD} -f ${TOOLSPATH}/mockery/Dockerfile --build-arg GOLANG=${GOBUILDIMAGE} --build-arg MOCKERY_VERSION=${MOCKERY_VERSION} -t ${MOCKERY_IMAGENAME}:$(MOCKERY_VERSION) .
|
||||
|
||||
|
|
|
@ -1206,6 +1206,12 @@ paths:
|
|||
- $ref: '#/parameters/projectName'
|
||||
- $ref: '#/parameters/repositoryName'
|
||||
- $ref: '#/parameters/reference'
|
||||
- name: scanType
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ScanType'
|
||||
description: 'The scan type: Vulnerabilities, SBOM'
|
||||
responses:
|
||||
'202':
|
||||
$ref: '#/responses/202'
|
||||
|
@ -1243,6 +1249,8 @@ paths:
|
|||
description: Successfully get scan log file
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
|
@ -9978,3 +9986,11 @@ definitions:
|
|||
items:
|
||||
type: string
|
||||
description: Links of the vulnerability
|
||||
|
||||
ScanType:
|
||||
type: object
|
||||
properties:
|
||||
scan_type:
|
||||
type: string
|
||||
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
|
||||
enum: [ vulnerability, sbom ]
|
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
|
@ -115,6 +115,11 @@ trivy:
|
|||
#
|
||||
# insecure The flag to skip verifying registry certificate
|
||||
insecure: false
|
||||
#
|
||||
# timeout The duration to wait for scan completion.
|
||||
# There is upper bound of 30 minutes defined in scan job. So if this `timeout` is larger than 30m0s, it will also timeout at 30m0s.
|
||||
timeout: 5m0s
|
||||
#
|
||||
# github_token The GitHub access token to download Trivy DB
|
||||
#
|
||||
# Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough
|
||||
|
|
|
@ -20,12 +20,12 @@ table artifact:
|
|||
/*
|
||||
Add new column artifact_type for artifact table to work with oci-spec v1.1.0 list referrer api
|
||||
*/
|
||||
ALTER TABLE artifact ADD COLUMN artifact_type varchar(255);
|
||||
ALTER TABLE artifact ADD COLUMN IF NOT EXISTS artifact_type varchar(255);
|
||||
|
||||
/*
|
||||
set value for artifact_type
|
||||
then set column artifact_type as not null
|
||||
*/
|
||||
UPDATE artifact SET artifact_type = media_type;
|
||||
UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL;
|
||||
|
||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.21.8
|
||||
FROM golang:1.22.2
|
||||
|
||||
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
|
||||
ENV BUILDTAGS include_oss include_gcs
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.21.8
|
||||
FROM golang:1.22.2
|
||||
|
||||
ADD . /go/src/github.com/aquasecurity/harbor-scanner-trivy/
|
||||
WORKDIR /go/src/github.com/aquasecurity/harbor-scanner-trivy/
|
||||
|
|
|
@ -19,7 +19,7 @@ TEMP=$(mktemp -d ${TMPDIR-/tmp}/trivy-adapter.XXXXXX)
|
|||
git clone https://github.com/aquasecurity/harbor-scanner-trivy.git $TEMP
|
||||
cd $TEMP; git checkout $VERSION; cd -
|
||||
|
||||
echo "Building Trivy adapter binary based on golang:1.21.8..."
|
||||
echo "Building Trivy adapter binary based on golang:1.22.2..."
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t trivy-adapter-golang $TEMP
|
||||
|
||||
|
|
|
@ -313,11 +313,11 @@ func ValidateCronString(cron string) error {
|
|||
// sort.Slice(input, func(i, j int) bool {
|
||||
// return MostMatchSorter(input[i].GroupName, input[j].GroupName, matchWord)
|
||||
// })
|
||||
//
|
||||
// a is the field to be used for sorting, b is the other field, matchWord is the word to be matched
|
||||
// the return value is true if a is less than b
|
||||
// for example, search with "user", input is {"harbor_user", "user", "users, "admin_user"}
|
||||
// it returns with this order {"user", "users", "admin_user", "harbor_user"}
|
||||
|
||||
func MostMatchSorter(a, b string, matchWord string) bool {
|
||||
// exact match always first
|
||||
if a == matchWord {
|
||||
|
@ -333,7 +333,7 @@ func MostMatchSorter(a, b string, matchWord string) bool {
|
|||
return len(a) < len(b)
|
||||
}
|
||||
|
||||
// IsLocalPath checks if path is local
|
||||
// IsLocalPath checks if path is local, includes the empty path
|
||||
func IsLocalPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")
|
||||
return len(path) == 0 || (strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//"))
|
||||
}
|
||||
|
|
|
@ -501,6 +501,7 @@ func TestIsLocalPath(t *testing.T) {
|
|||
{"other_site1", args{"//www.myexample.com"}, false},
|
||||
{"other_site2", args{"https://www.myexample.com"}, false},
|
||||
{"other_site", args{"http://www.myexample.com"}, false},
|
||||
{"empty_path", args{""}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -92,6 +92,7 @@ func parseV1alpha1Icon(artifact *artifact.Artifact, manifest *v1.Manifest, reg r
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer icon.Close()
|
||||
// check the size of the size <= 1MB
|
||||
data, err := io.ReadAll(io.LimitReader(icon, 1<<20))
|
||||
if err != nil {
|
||||
|
|
|
@ -85,11 +85,11 @@ func (p *processor) AbstractAddition(_ context.Context, artifact *artifact.Artif
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer blob.Close()
|
||||
content, err := io.ReadAll(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blob.Close()
|
||||
chartDetails, err := p.chartOperator.GetDetails(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// 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 sbom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
)
|
||||
|
||||
const (
|
||||
// processorArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor
|
||||
processorArtifactTypeSBOM = "SBOM"
|
||||
// processorMediaType is the media type for SBOM, it's scope is only used to register the processor
|
||||
processorMediaType = "application/vnd.goharbor.harbor.sbom.v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pc := &Processor{}
|
||||
pc.ManifestProcessor = base.NewManifestProcessor()
|
||||
if err := processor.Register(pc, processorMediaType); err != nil {
|
||||
log.Errorf("failed to register processor for media type %s: %v", processorMediaType, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Processor is the processor for SBOM
|
||||
type Processor struct {
|
||||
*base.ManifestProcessor
|
||||
}
|
||||
|
||||
// AbstractAddition returns the addition for SBOM
|
||||
func (m *Processor) AbstractAddition(_ context.Context, art *artifact.Artifact, _ string) (*processor.Addition, error) {
|
||||
man, _, err := m.RegCli.PullManifest(art.RepositoryName, art.Digest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to pull manifest")
|
||||
}
|
||||
_, payload, err := man.Payload()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get payload")
|
||||
}
|
||||
manifest := &v1.Manifest{}
|
||||
if err := json.Unmarshal(payload, manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// SBOM artifact should only have one layer
|
||||
if len(manifest.Layers) != 1 {
|
||||
return nil, errors.New(nil).WithCode(errors.NotFoundCode).WithMessage("The sbom is not found")
|
||||
}
|
||||
layerDgst := manifest.Layers[0].Digest.String()
|
||||
_, blob, err := m.RegCli.PullBlob(art.RepositoryName, layerDgst)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to pull the blob")
|
||||
}
|
||||
defer blob.Close()
|
||||
content, err := io.ReadAll(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &processor.Addition{
|
||||
Content: content,
|
||||
ContentType: processorMediaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetArtifactType the artifact type is used to display the artifact type in the UI
|
||||
func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string {
|
||||
return processorArtifactTypeSBOM
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// 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 sbom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/base"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/registry"
|
||||
)
|
||||
|
||||
type SBOMProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
processor *Processor
|
||||
regCli *registry.Client
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) SetupSuite() {
|
||||
suite.regCli = ®istry.Client{}
|
||||
suite.processor = &Processor{
|
||||
&base.ManifestProcessor{
|
||||
RegCli: suite.regCli,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TearDownSuite() {
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TestAbstractAdditionNormal() {
|
||||
manContent := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:abc"
|
||||
}]
|
||||
}`
|
||||
sbomContent := "this is a sbom content"
|
||||
reader := strings.NewReader(sbomContent)
|
||||
blobReader := io.NopCloser(reader)
|
||||
mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent))
|
||||
suite.Require().NoError(err)
|
||||
suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once()
|
||||
suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(int64(123), blobReader, nil).Once()
|
||||
addition, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom")
|
||||
suite.Nil(err)
|
||||
suite.Equal(sbomContent, string(addition.Content))
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TestAbstractAdditionMultiLayer() {
|
||||
manContent := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:abc"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 843,
|
||||
"digest": "sha256:def"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 531,
|
||||
"digest": "sha256:123"
|
||||
}
|
||||
]
|
||||
}`
|
||||
mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent))
|
||||
suite.Require().NoError(err)
|
||||
suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once()
|
||||
_, err = suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom")
|
||||
suite.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullBlobError() {
|
||||
manContent := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:abc"
|
||||
}
|
||||
]
|
||||
}`
|
||||
mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent))
|
||||
suite.Require().NoError(err)
|
||||
suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once()
|
||||
suite.regCli.On("PullBlob", mock.Anything, mock.Anything).Return(int64(123), nil, errors.NotFoundError(fmt.Errorf("not found"))).Once()
|
||||
addition, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom")
|
||||
suite.NotNil(err)
|
||||
suite.Nil(addition)
|
||||
}
|
||||
func (suite *SBOMProcessorTestSuite) TestAbstractAdditionNoSBOMLayer() {
|
||||
manContent := `{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:e91b9dfcbbb3b88bac94726f276b89de46e4460b55f6e6d6f876e666b150ec5b",
|
||||
"size": 498
|
||||
}
|
||||
}`
|
||||
mani, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(manContent))
|
||||
suite.Require().NoError(err)
|
||||
suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(mani, "sha256:123", nil).Once()
|
||||
_, err = suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom")
|
||||
suite.NotNil(err)
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullManifestError() {
|
||||
suite.regCli.On("PullManifest", mock.Anything, mock.Anything).Return(nil, "sha256:123", errors.NotFoundError(fmt.Errorf("not found"))).Once()
|
||||
_, err := suite.processor.AbstractAddition(context.Background(), &artifact.Artifact{RepositoryName: "repo", Digest: "digest"}, "sbom")
|
||||
suite.NotNil(err)
|
||||
|
||||
}
|
||||
|
||||
func (suite *SBOMProcessorTestSuite) TestGetArtifactType() {
|
||||
suite.Equal(processorArtifactTypeSBOM, suite.processor.GetArtifactType(context.Background(), &artifact.Artifact{}))
|
||||
}
|
||||
|
||||
func TestSBOMProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &SBOMProcessorTestSuite{})
|
||||
}
|
|
@ -258,6 +258,11 @@ func (a *ArtifactEventHandler) onPush(ctx context.Context, event *event.Artifact
|
|||
if err := autoScan(ctx, &artifact.Artifact{Artifact: *event.Artifact}, event.Tags...); err != nil {
|
||||
log.Errorf("scan artifact %s@%s failed, error: %v", event.Artifact.RepositoryName, event.Artifact.Digest, err)
|
||||
}
|
||||
|
||||
log.Debugf("auto generate sbom is triggered for artifact event %+v", event)
|
||||
if err := autoGenSBOM(ctx, &artifact.Artifact{Artifact: *event.Artifact}); err != nil {
|
||||
log.Errorf("generate sbom for artifact %s@%s failed, error: %v", event.Artifact.RepositoryName, event.Artifact.Digest, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
)
|
||||
|
||||
|
@ -43,3 +44,21 @@ func autoScan(ctx context.Context, a *artifact.Artifact, tags ...string) error {
|
|||
return scan.DefaultController.Scan(ctx, a, options...)
|
||||
})(orm.SetTransactionOpNameToContext(ctx, "tx-auto-scan"))
|
||||
}
|
||||
|
||||
func autoGenSBOM(ctx context.Context, a *artifact.Artifact) error {
|
||||
proj, err := project.Ctl.Get(ctx, a.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !proj.AutoSBOMGen() {
|
||||
return nil
|
||||
}
|
||||
// transaction here to work with the image index
|
||||
return orm.WithTransaction(func(ctx context.Context) error {
|
||||
options := []scan.Option{}
|
||||
// TODO: extract the sbom scan type to a constant
|
||||
options = append(options, scan.WithScanType("sbom"))
|
||||
log.Debugf("sbom scan controller artifact %+v, options %+v", a, options)
|
||||
return scan.DefaultController.Scan(ctx, a, options...)
|
||||
})(orm.SetTransactionOpNameToContext(ctx, "tx-auto-gen-sbom"))
|
||||
}
|
||||
|
|
|
@ -95,6 +95,36 @@ func (suite *AutoScanTestSuite) TestAutoScan() {
|
|||
suite.Nil(autoScan(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanSBOM() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaAutoSBOMGen: "true",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mock.OnAnything(suite.scanController, "Scan").Return(nil)
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
art := &artifact.Artifact{}
|
||||
|
||||
suite.Nil(autoGenSBOM(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanSBOMFalse() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
proModels.ProMetaAutoSBOMGen: "false",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mock.OnAnything(suite.scanController, "Scan").Return(nil)
|
||||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
art := &artifact.Artifact{}
|
||||
|
||||
suite.Nil(autoGenSBOM(ctx, art))
|
||||
}
|
||||
|
||||
func (suite *AutoScanTestSuite) TestAutoScanFailed() {
|
||||
mock.OnAnything(suite.projectController, "Get").Return(&proModels.Project{
|
||||
Metadata: map[string]string{
|
||||
|
|
|
@ -69,6 +69,10 @@ var (
|
|||
path: "./icons/wasm.png",
|
||||
resize: true,
|
||||
},
|
||||
icon.DigestOfIconAccSBOM: {
|
||||
path: "./icons/sbom.png",
|
||||
resize: true,
|
||||
},
|
||||
icon.DigestOfIconDefault: {
|
||||
path: "./icons/default.png",
|
||||
resize: true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package flow
|
||||
|
||||
|
@ -18,6 +18,10 @@ type mockFactory struct {
|
|||
func (_m *mockFactory) AdapterPattern() *model.AdapterPattern {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AdapterPattern")
|
||||
}
|
||||
|
||||
var r0 *model.AdapterPattern
|
||||
if rf, ok := ret.Get(0).(func() *model.AdapterPattern); ok {
|
||||
r0 = rf()
|
||||
|
@ -34,6 +38,10 @@ func (_m *mockFactory) AdapterPattern() *model.AdapterPattern {
|
|||
func (_m *mockFactory) Create(_a0 *model.Registry) (adapter.Adapter, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 adapter.Adapter
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*model.Registry) (adapter.Adapter, error)); ok {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package flow
|
||||
|
||||
|
@ -21,6 +21,10 @@ type mockAdapter struct {
|
|||
func (_m *mockAdapter) BlobExist(repository string, digest string) (bool, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for BlobExist")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) (bool, error)); ok {
|
||||
|
@ -45,6 +49,10 @@ func (_m *mockAdapter) BlobExist(repository string, digest string) (bool, error)
|
|||
func (_m *mockAdapter) CanBeMount(digest string) (bool, string, error) {
|
||||
ret := _m.Called(digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CanBeMount")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 string
|
||||
var r2 error
|
||||
|
@ -76,6 +84,10 @@ func (_m *mockAdapter) CanBeMount(digest string) (bool, string, error) {
|
|||
func (_m *mockAdapter) DeleteManifest(repository string, reference string) error {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteManifest")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, reference)
|
||||
|
@ -90,6 +102,10 @@ func (_m *mockAdapter) DeleteManifest(repository string, reference string) error
|
|||
func (_m *mockAdapter) DeleteTag(repository string, tag string) error {
|
||||
ret := _m.Called(repository, tag)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteTag")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, tag)
|
||||
|
@ -104,6 +120,10 @@ func (_m *mockAdapter) DeleteTag(repository string, tag string) error {
|
|||
func (_m *mockAdapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) {
|
||||
ret := _m.Called(filters)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FetchArtifacts")
|
||||
}
|
||||
|
||||
var r0 []*model.Resource
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]*model.Filter) ([]*model.Resource, error)); ok {
|
||||
|
@ -130,6 +150,10 @@ func (_m *mockAdapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resourc
|
|||
func (_m *mockAdapter) HealthCheck() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for HealthCheck")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
|
@ -154,6 +178,10 @@ func (_m *mockAdapter) HealthCheck() (string, error) {
|
|||
func (_m *mockAdapter) Info() (*model.RegistryInfo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Info")
|
||||
}
|
||||
|
||||
var r0 *model.RegistryInfo
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (*model.RegistryInfo, error)); ok {
|
||||
|
@ -180,6 +208,10 @@ func (_m *mockAdapter) Info() (*model.RegistryInfo, error) {
|
|||
func (_m *mockAdapter) ListTags(repository string) ([]string, error) {
|
||||
ret := _m.Called(repository)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListTags")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
|
||||
|
@ -206,6 +238,10 @@ func (_m *mockAdapter) ListTags(repository string) ([]string, error) {
|
|||
func (_m *mockAdapter) ManifestExist(repository string, reference string) (bool, *distribution.Descriptor, error) {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ManifestExist")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 *distribution.Descriptor
|
||||
var r2 error
|
||||
|
@ -239,6 +275,10 @@ func (_m *mockAdapter) ManifestExist(repository string, reference string) (bool,
|
|||
func (_m *mockAdapter) MountBlob(srcRepository string, digest string, dstRepository string) error {
|
||||
ret := _m.Called(srcRepository, digest, dstRepository)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MountBlob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(srcRepository, digest, dstRepository)
|
||||
|
@ -253,6 +293,10 @@ func (_m *mockAdapter) MountBlob(srcRepository string, digest string, dstReposit
|
|||
func (_m *mockAdapter) PrepareForPush(_a0 []*model.Resource) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PrepareForPush")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]*model.Resource) error); ok {
|
||||
r0 = rf(_a0)
|
||||
|
@ -267,6 +311,10 @@ func (_m *mockAdapter) PrepareForPush(_a0 []*model.Resource) error {
|
|||
func (_m *mockAdapter) PullBlob(repository string, digest string) (int64, io.ReadCloser, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullBlob")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 io.ReadCloser
|
||||
var r2 error
|
||||
|
@ -300,6 +348,10 @@ func (_m *mockAdapter) PullBlob(repository string, digest string) (int64, io.Rea
|
|||
func (_m *mockAdapter) PullBlobChunk(repository string, digest string, blobSize int64, start int64, end int64) (int64, io.ReadCloser, error) {
|
||||
ret := _m.Called(repository, digest, blobSize, start, end)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullBlobChunk")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 io.ReadCloser
|
||||
var r2 error
|
||||
|
@ -340,6 +392,10 @@ func (_m *mockAdapter) PullManifest(repository string, reference string, acceptt
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullManifest")
|
||||
}
|
||||
|
||||
var r0 distribution.Manifest
|
||||
var r1 string
|
||||
var r2 error
|
||||
|
@ -373,6 +429,10 @@ func (_m *mockAdapter) PullManifest(repository string, reference string, acceptt
|
|||
func (_m *mockAdapter) PushBlob(repository string, digest string, size int64, blob io.Reader) error {
|
||||
ret := _m.Called(repository, digest, size, blob)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushBlob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, int64, io.Reader) error); ok {
|
||||
r0 = rf(repository, digest, size, blob)
|
||||
|
@ -387,6 +447,10 @@ func (_m *mockAdapter) PushBlob(repository string, digest string, size int64, bl
|
|||
func (_m *mockAdapter) PushBlobChunk(repository string, digest string, size int64, chunk io.Reader, start int64, end int64, location string) (string, int64, error) {
|
||||
ret := _m.Called(repository, digest, size, chunk, start, end, location)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushBlobChunk")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 int64
|
||||
var r2 error
|
||||
|
@ -418,6 +482,10 @@ func (_m *mockAdapter) PushBlobChunk(repository string, digest string, size int6
|
|||
func (_m *mockAdapter) PushManifest(repository string, reference string, mediaType string, payload []byte) (string, error) {
|
||||
ret := _m.Called(repository, reference, mediaType, payload)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PushManifest")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, []byte) (string, error)); ok {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package replication
|
||||
|
||||
|
@ -21,6 +21,10 @@ type flowController struct {
|
|||
func (_m *flowController) Start(ctx context.Context, executionID int64, policy *model.Policy, resource *regmodel.Resource) error {
|
||||
ret := _m.Called(ctx, executionID, policy, resource)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Start")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *model.Policy, *regmodel.Resource) error); ok {
|
||||
r0 = rf(ctx, executionID, policy, resource)
|
||||
|
|
|
@ -17,6 +17,7 @@ package scan
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
@ -25,7 +26,6 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
ar "github.com/goharbor/harbor/src/controller/artifact"
|
||||
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
|
@ -91,6 +91,7 @@ type launchScanJobParam struct {
|
|||
Artifact *ar.Artifact
|
||||
Tag string
|
||||
Reports []*scan.Report
|
||||
Type string
|
||||
}
|
||||
|
||||
// basicController is default implementation of api.Controller interface
|
||||
|
@ -287,6 +288,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
|||
Artifact: art,
|
||||
Tag: tag,
|
||||
Reports: reports,
|
||||
Type: opts.GetScanType(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -339,15 +341,16 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
|||
}
|
||||
|
||||
// Stop scan job of a given artifact
|
||||
func (bc *basicController) Stop(ctx context.Context, artifact *ar.Artifact) error {
|
||||
func (bc *basicController) Stop(ctx context.Context, artifact *ar.Artifact, capType string) error {
|
||||
if artifact == nil {
|
||||
return errors.New("nil artifact to stop scan")
|
||||
}
|
||||
query := q.New(q.KeyWords{"extra_attrs.artifact.digest": artifact.Digest})
|
||||
query := q.New(q.KeyWords{"vendor_type": job.ImageScanJobVendorType, "extra_attrs.artifact.digest": artifact.Digest, "extra_attrs.enabled_capabilities.type": capType})
|
||||
executions, err := bc.execMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(executions) == 0 {
|
||||
message := fmt.Sprintf("no scan job for artifact digest=%v", artifact.Digest)
|
||||
return errors.BadRequestError(nil).WithMessage(message)
|
||||
|
@ -672,12 +675,23 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
|
|||
return reports, nil
|
||||
}
|
||||
|
||||
func isSBOMMimeTypes(mimeTypes []string) bool {
|
||||
for _, mimeType := range mimeTypes {
|
||||
if mimeType == v1.MimeTypeSBOMReport {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSummary ...
|
||||
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
|
||||
if artifact == nil {
|
||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
||||
}
|
||||
|
||||
if isSBOMMimeTypes(mimeTypes) {
|
||||
return bc.GetSBOMSummary(ctx, artifact, mimeTypes)
|
||||
}
|
||||
// Get reports first
|
||||
rps, err := bc.GetReport(ctx, artifact, mimeTypes)
|
||||
if err != nil {
|
||||
|
@ -706,6 +720,30 @@ func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact
|
|||
return summaries, nil
|
||||
}
|
||||
|
||||
func (bc *basicController) GetSBOMSummary(ctx context.Context, art *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
|
||||
if art == nil {
|
||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
||||
}
|
||||
r, err := bc.sc.GetRegistrationByProject(ctx, art.ProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "scan controller: get sbom summary")
|
||||
}
|
||||
reports, err := bc.manager.GetBy(ctx, art.Digest, r.UUID, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) == 0 {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
reportContent := reports[0].Report
|
||||
if len(reportContent) == 0 {
|
||||
log.Warning("no content for current report")
|
||||
}
|
||||
result := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(reportContent), &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetScanLog ...
|
||||
func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact, uuid string) ([]byte, error) {
|
||||
if len(uuid) == 0 {
|
||||
|
@ -912,7 +950,7 @@ func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artif
|
|||
}
|
||||
|
||||
// makeRobotAccount creates a robot account based on the arguments for scanning.
|
||||
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration) (*robot.Robot, error) {
|
||||
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration, permission []*types.Policy) (*robot.Robot, error) {
|
||||
// Use uuid as name to avoid duplicated entries.
|
||||
UUID, err := bc.uuid()
|
||||
if err != nil {
|
||||
|
@ -934,16 +972,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
|
|||
{
|
||||
Kind: "project",
|
||||
Namespace: projectName,
|
||||
Access: []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
},
|
||||
Access: permission,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -980,7 +1009,12 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
|
|||
return errors.Wrap(err, "scan controller: launch scan job")
|
||||
}
|
||||
|
||||
robot, err := bc.makeRobotAccount(ctx, param.Artifact.ProjectID, param.Artifact.RepositoryName, param.Registration)
|
||||
// Get Scanner handler by scan type to separate the scan logic for different scan types
|
||||
handler := sca.GetScanHandler(param.Type)
|
||||
if handler == nil {
|
||||
return fmt.Errorf("failed to get scan handler, type is %v", param.Type)
|
||||
}
|
||||
robot, err := bc.makeRobotAccount(ctx, param.Artifact.ProjectID, param.Artifact.RepositoryName, param.Registration, handler.RequiredPermissions())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scan controller: launch scan job")
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import (
|
|||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||
robottesting "github.com/goharbor/harbor/src/testing/controller/robot"
|
||||
|
@ -77,7 +78,7 @@ type ControllerTestSuite struct {
|
|||
taskMgr *tasktesting.Manager
|
||||
reportMgr *reporttesting.Manager
|
||||
ar artifact.Controller
|
||||
c Controller
|
||||
c *basicController
|
||||
reportConverter *postprocessorstesting.ScanReportV1ToV2Converter
|
||||
cache *mockcache.Cache
|
||||
}
|
||||
|
@ -179,7 +180,19 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||
},
|
||||
}
|
||||
|
||||
sbomReport := []*scan.Report{
|
||||
{
|
||||
ID: 12,
|
||||
UUID: "rp-uuid-002",
|
||||
Digest: "digest-code",
|
||||
RegistrationUUID: "uuid001",
|
||||
MimeType: "application/vnd.scanner.adapter.sbom.report.harbor+json; version=1.0",
|
||||
Status: "Success",
|
||||
Report: `{"sbom_digest": "sha256:1234567890", "scan_status": "Success", "duration": 3, "start_time": "2021-09-01T00:00:00Z", "end_time": "2021-09-01T00:00:03Z"}`,
|
||||
},
|
||||
}
|
||||
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeNativeReport}).Return(reports, nil)
|
||||
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(sbomReport, nil)
|
||||
mgr.On("Get", mock.Anything, "rp-uuid-001").Return(reports[0], nil)
|
||||
mgr.On("UpdateReportData", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil)
|
||||
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
|
||||
|
@ -380,7 +393,7 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
|||
func (suite *ControllerTestSuite) TestScanControllerStop() {
|
||||
{
|
||||
// artifact not provieded
|
||||
suite.Require().Error(suite.c.Stop(context.TODO(), nil))
|
||||
suite.Require().Error(suite.c.Stop(context.TODO(), nil, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -392,7 +405,7 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
|||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().NoError(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().NoError(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -402,7 +415,7 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
|||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -411,7 +424,7 @@ func (suite *ControllerTestSuite) TestScanControllerStop() {
|
|||
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact))
|
||||
suite.Require().Error(suite.c.Stop(ctx, suite.artifact, "vulnerability"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -619,3 +632,26 @@ func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs .
|
|||
|
||||
return extraAttrs
|
||||
}
|
||||
|
||||
func (suite *ControllerTestSuite) TestGenerateSBOMSummary() {
|
||||
sum, err := suite.c.GetSBOMSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeSBOMReport})
|
||||
suite.Nil(err)
|
||||
suite.NotNil(sum)
|
||||
status := sum["scan_status"]
|
||||
suite.NotNil(status)
|
||||
dgst := sum["sbom_digest"]
|
||||
suite.NotNil(dgst)
|
||||
suite.Equal("Success", status)
|
||||
suite.Equal("sha256:1234567890", dgst)
|
||||
}
|
||||
|
||||
func TestIsSBOMMimeTypes(t *testing.T) {
|
||||
// Test with a slice containing the SBOM mime type
|
||||
assert.True(t, isSBOMMimeTypes([]string{v1.MimeTypeSBOMReport}))
|
||||
|
||||
// Test with a slice not containing the SBOM mime type
|
||||
assert.False(t, isSBOMMimeTypes([]string{"application/vnd.oci.image.manifest.v1+json"}))
|
||||
|
||||
// Test with an empty slice
|
||||
assert.False(t, isSBOMMimeTypes([]string{}))
|
||||
}
|
||||
|
|
|
@ -55,10 +55,11 @@ type Controller interface {
|
|||
// Arguments:
|
||||
// ctx context.Context : the context for this method
|
||||
// artifact *artifact.Artifact : the artifact whose scan job to be stopped
|
||||
// capType string : the capability type of the scanner, vulnerability or SBOM.
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
Stop(ctx context.Context, artifact *artifact.Artifact) error
|
||||
Stop(ctx context.Context, artifact *artifact.Artifact, capType string) error
|
||||
|
||||
// GetReport gets the reports for the given artifact identified by the digest
|
||||
//
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
package scan
|
||||
|
||||
import v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
|
||||
// Options keep the settings/configurations for scanning.
|
||||
type Options struct {
|
||||
ExecutionID int64 // The execution id to scan artifact
|
||||
|
@ -24,7 +26,7 @@ type Options struct {
|
|||
// GetScanType returns the scan type. for backward compatibility, the default type is vulnerability.
|
||||
func (o *Options) GetScanType() string {
|
||||
if len(o.ScanType) == 0 {
|
||||
o.ScanType = "vulnerability"
|
||||
o.ScanType = v1.ScanTypeVulnerability
|
||||
}
|
||||
return o.ScanType
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ import (
|
|||
"github.com/goharbor/harbor/src/pkg/oidc"
|
||||
"github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
pkguser "github.com/goharbor/harbor/src/pkg/user"
|
||||
"github.com/goharbor/harbor/src/pkg/version"
|
||||
"github.com/goharbor/harbor/src/server"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/goharbor/harbor/src
|
||||
|
||||
go 1.21
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/FZambia/sentinel v1.1.0
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/rest"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package mgt
|
||||
|
||||
|
@ -18,6 +18,10 @@ type MockManager struct {
|
|||
func (_m *MockManager) GetJob(jobID string) (*job.Stats, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJob")
|
||||
}
|
||||
|
||||
var r0 *job.Stats
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*job.Stats, error)); ok {
|
||||
|
@ -44,6 +48,10 @@ func (_m *MockManager) GetJob(jobID string) (*job.Stats, error) {
|
|||
func (_m *MockManager) GetJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJobs")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
|
@ -77,6 +85,10 @@ func (_m *MockManager) GetJobs(q *query.Parameter) ([]*job.Stats, int64, error)
|
|||
func (_m *MockManager) GetPeriodicExecution(pID string, q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(pID, q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPeriodicExecution")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
|
@ -110,6 +122,10 @@ func (_m *MockManager) GetPeriodicExecution(pID string, q *query.Parameter) ([]*
|
|||
func (_m *MockManager) GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
|
||||
ret := _m.Called(q)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScheduledJobs")
|
||||
}
|
||||
|
||||
var r0 []*job.Stats
|
||||
var r1 int64
|
||||
var r2 error
|
||||
|
@ -143,6 +159,10 @@ func (_m *MockManager) GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64
|
|||
func (_m *MockManager) SaveJob(_a0 *job.Stats) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveJob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*job.Stats) error); ok {
|
||||
r0 = rf(_a0)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package period
|
||||
|
||||
|
@ -13,6 +13,10 @@ type MockScheduler struct {
|
|||
func (_m *MockScheduler) Schedule(policy *Policy) (int64, error) {
|
||||
ret := _m.Called(policy)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Schedule")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*Policy) (int64, error)); ok {
|
||||
|
@ -42,6 +46,10 @@ func (_m *MockScheduler) Start() {
|
|||
func (_m *MockScheduler) UnSchedule(policyID string) error {
|
||||
ret := _m.Called(policyID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UnSchedule")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(policyID)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package cache
|
||||
|
||||
|
@ -18,6 +18,10 @@ type mockCache struct {
|
|||
func (_m *mockCache) Contains(ctx context.Context, key string) bool {
|
||||
ret := _m.Called(ctx, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Contains")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = rf(ctx, key)
|
||||
|
@ -32,6 +36,10 @@ func (_m *mockCache) Contains(ctx context.Context, key string) bool {
|
|||
func (_m *mockCache) Delete(ctx context.Context, key string) error {
|
||||
ret := _m.Called(ctx, key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, key)
|
||||
|
@ -46,6 +54,10 @@ func (_m *mockCache) Delete(ctx context.Context, key string) error {
|
|||
func (_m *mockCache) Fetch(ctx context.Context, key string, value interface{}) error {
|
||||
ret := _m.Called(ctx, key, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Fetch")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok {
|
||||
r0 = rf(ctx, key, value)
|
||||
|
@ -60,6 +72,10 @@ func (_m *mockCache) Fetch(ctx context.Context, key string, value interface{}) e
|
|||
func (_m *mockCache) Ping(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ping")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
|
@ -81,6 +97,10 @@ func (_m *mockCache) Save(ctx context.Context, key string, value interface{}, ex
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, interface{}, ...time.Duration) error); ok {
|
||||
r0 = rf(ctx, key, value, expiration...)
|
||||
|
@ -95,6 +115,10 @@ func (_m *mockCache) Save(ctx context.Context, key string, value interface{}, ex
|
|||
func (_m *mockCache) Scan(ctx context.Context, match string) (Iterator, error) {
|
||||
ret := _m.Called(ctx, match)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Scan")
|
||||
}
|
||||
|
||||
var r0 Iterator
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (Iterator, error)); ok {
|
||||
|
|
|
@ -27,4 +27,5 @@ const (
|
|||
DigestOfIconAccCosign = "sha256:20401d5b3a0f6dbc607c8d732eb08471af4ae6b19811a4efce8c6a724aed2882"
|
||||
DigestOfIconAccNotation = "sha256:3ac706e102bbe9362b400aa162df58135d35e66b9c3bee2165de92022d25fe34"
|
||||
DigestOfIconAccNydus = "sha256:dfcb6617cd9c144358dc1b305b87bbe34f0b619f1e329116e6aee2e41f2e34cf"
|
||||
DigestOfIconAccSBOM = "sha256:c19f80c357cd7e90d2a01b9ae3e2eb62ce447a2662bb590a19177d72d550bdae"
|
||||
)
|
||||
|
|
|
@ -33,6 +33,7 @@ var (
|
|||
model.TypeCosignSignature: icon.DigestOfIconAccCosign,
|
||||
model.TypeNotationSignature: icon.DigestOfIconAccNotation,
|
||||
model.TypeNydusAccelerator: icon.DigestOfIconAccNydus,
|
||||
model.TypeHarborSBOM: icon.DigestOfIconAccSBOM,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ func (c *Client) getProjects() ([]*Project, error) {
|
|||
|
||||
func (c *Client) getProjectsByName(name string) ([]*Project, error) {
|
||||
var projects []*Project
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects?search=%s&membership=true&search_namespaces=true&per_page=50", c.url, name)
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects?search=%s&search_namespaces=true&per_page=50", c.url, name)
|
||||
if err := c.GetAndIteratePagination(urlAPI, &projects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// 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 scan
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
var handlerRegistry = map[string]Handler{}
|
||||
|
||||
// RegisterScanHanlder register scanner handler
|
||||
func RegisterScanHanlder(requestType string, handler Handler) {
|
||||
handlerRegistry[requestType] = handler
|
||||
}
|
||||
|
||||
// GetScanHandler get the handler
|
||||
func GetScanHandler(requestType string) Handler {
|
||||
return handlerRegistry[requestType]
|
||||
}
|
||||
|
||||
// Handler handler for scan job, it could be implement by different scan type, such as vulnerability, sbom
|
||||
type Handler interface {
|
||||
RequiredPermissions() []*types.Policy
|
||||
// PostScan defines the operation after scan
|
||||
PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
|
||||
}
|
|
@ -16,6 +16,7 @@ package scan
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -34,8 +35,8 @@ import (
|
|||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
@ -145,6 +146,7 @@ func (j *Job) Validate(params job.Parameters) error {
|
|||
func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
// Get logger
|
||||
myLogger := ctx.GetLogger()
|
||||
startTime := time.Now()
|
||||
|
||||
// shouldStop checks if the job should be stopped
|
||||
shouldStop := func() bool {
|
||||
|
@ -160,6 +162,11 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||
r, _ := extractRegistration(params)
|
||||
req, _ := ExtractScanReq(params)
|
||||
mimeTypes, _ := extractMimeTypes(params)
|
||||
scanType := v1.ScanTypeVulnerability
|
||||
if len(req.RequestType) > 0 {
|
||||
scanType = req.RequestType[0].Type
|
||||
}
|
||||
handler := GetScanHandler(scanType)
|
||||
|
||||
// Print related infos to log
|
||||
printJSONParameter(JobParamRegistration, removeRegistrationAuthInfo(r), myLogger)
|
||||
|
@ -235,30 +242,19 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||
}
|
||||
|
||||
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
|
||||
|
||||
rawReport, err := client.GetScanReport(resp.ID, m)
|
||||
rawReport, err := fetchScanReportFromScanner(client, resp.ID, m)
|
||||
if err != nil {
|
||||
// Not ready yet
|
||||
if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok {
|
||||
// Reset to the new check interval
|
||||
tm.Reset(time.Duration(notReadyErr.RetryAfter) * time.Second)
|
||||
myLogger.Infof("Report with mime type %s is not ready yet, retry after %d seconds", m, notReadyErr.RetryAfter)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
errs[i] = errors.Wrap(err, fmt.Sprintf("check scan report with mime type %s", m))
|
||||
errs[i] = errors.Wrap(err, fmt.Sprintf("scan job: fetch scan report, mimetype %v", m))
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the data is aligned with the v1 spec.
|
||||
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
||||
errs[i] = errors.Wrap(err, "scan job: resolve report data")
|
||||
return
|
||||
}
|
||||
|
||||
rawReports[i] = rawReport
|
||||
|
||||
return
|
||||
case <-ctx.SystemContext().Done():
|
||||
// Terminated by system
|
||||
|
@ -292,33 +288,19 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||
// Log error to the job log
|
||||
if err != nil {
|
||||
myLogger.Error(err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for i, mimeType := range mimeTypes {
|
||||
reports, err := report.Mgr.GetBy(ctx.SystemContext(), req.Artifact.Digest, r.UUID, []string{mimeType})
|
||||
rp, err := getReportPlaceholder(ctx.SystemContext(), req.Artifact.Digest, r.UUID, mimeType, myLogger)
|
||||
if err != nil {
|
||||
myLogger.Error("Failed to get report for artifact %s of mimetype %s, error %v", req.Artifact.Digest, mimeType, err)
|
||||
|
||||
return err
|
||||
}
|
||||
myLogger.Debugf("Converting report ID %s to the new V2 schema", rp.UUID)
|
||||
|
||||
if len(reports) == 0 {
|
||||
myLogger.Error("No report found for artifact %s of mimetype %s, error %v", req.Artifact.Digest, mimeType, err)
|
||||
|
||||
return errors.NotFoundError(nil).WithMessage("no report found to update data")
|
||||
}
|
||||
|
||||
rp := reports[0]
|
||||
|
||||
logger.Debugf("Converting report ID %s to the new V2 schema", rp.UUID)
|
||||
|
||||
// use a new ormer here to use the short db connection
|
||||
_, reportData, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), rp.UUID, rp.RegistrationUUID, rp.Digest, rawReports[i])
|
||||
reportData, err := handler.PostScan(ctx, req, rp, rawReports[i], startTime, robotAccount)
|
||||
if err != nil {
|
||||
myLogger.Errorf("Failed to convert vulnerability data to new schema for report %s, error %v", rp.UUID, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -328,7 +310,6 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||
// would be redundant
|
||||
if err := report.Mgr.UpdateReportData(ctx.SystemContext(), rp.UUID, reportData); err != nil {
|
||||
myLogger.Errorf("Failed to update report data for report %s, error %v", rp.UUID, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -338,6 +319,31 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getReportPlaceholder(ctx context.Context, digest string, reportUUID string, mimeType string, logger logger.Interface) (*scan.Report, error) {
|
||||
reports, err := report.Mgr.GetBy(ctx, digest, reportUUID, []string{mimeType})
|
||||
if err != nil {
|
||||
logger.Error("Failed to get report for artifact %s of mimetype %s, error %v", digest, mimeType, err)
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) == 0 {
|
||||
logger.Errorf("No report found for artifact %s of mimetype %s, error %v", digest, mimeType, err)
|
||||
return nil, errors.NotFoundError(nil).WithMessage("no report found to update data")
|
||||
}
|
||||
return reports[0], nil
|
||||
}
|
||||
|
||||
func fetchScanReportFromScanner(client v1.Client, requestID string, m string) (rawReport string, err error) {
|
||||
rawReport, err = client.GetScanReport(requestID, m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Make sure the data is aligned with the v1 spec.
|
||||
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return rawReport, nil
|
||||
}
|
||||
|
||||
// ExtractScanReq extracts the scan request from the job parameters.
|
||||
func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
|
||||
v, ok := params[JobParameterRequest]
|
||||
|
|
|
@ -19,15 +19,19 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
mockjobservice "github.com/goharbor/harbor/src/testing/jobservice"
|
||||
mocktesting "github.com/goharbor/harbor/src/testing/mock"
|
||||
v1testing "github.com/goharbor/harbor/src/testing/pkg/scan/rest/v1"
|
||||
|
@ -35,10 +39,11 @@ import (
|
|||
|
||||
// JobTestSuite is a test suite to test the scan job.
|
||||
type JobTestSuite struct {
|
||||
suite.Suite
|
||||
htesting.Suite
|
||||
|
||||
defaultClientPool v1.ClientPool
|
||||
mcp *v1testing.ClientPool
|
||||
reportIDs []string
|
||||
}
|
||||
|
||||
// TestJob is the entry of JobTestSuite.
|
||||
|
@ -48,6 +53,7 @@ func TestJob(t *testing.T) {
|
|||
|
||||
// SetupSuite sets up test env for JobTestSuite.
|
||||
func (suite *JobTestSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
mcp := &v1testing.ClientPool{}
|
||||
suite.defaultClientPool = v1.DefaultClientPool
|
||||
v1.DefaultClientPool = mcp
|
||||
|
@ -55,9 +61,12 @@ func (suite *JobTestSuite) SetupSuite() {
|
|||
suite.mcp = mcp
|
||||
}
|
||||
|
||||
// TeraDownSuite clears test env for TeraDownSuite.
|
||||
func (suite *JobTestSuite) TeraDownSuite() {
|
||||
// TearDownSuite clears test env for TearDownSuite.
|
||||
func (suite *JobTestSuite) TearDownSuite() {
|
||||
v1.DefaultClientPool = suite.defaultClientPool
|
||||
for _, id := range suite.reportIDs {
|
||||
_ = report.Mgr.Delete(suite.Context(), id)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJob tests the scan job
|
||||
|
@ -151,3 +160,59 @@ func (suite *JobTestSuite) TestJob() {
|
|||
err = j.Run(ctx, jp)
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
func (suite *JobTestSuite) TestgetReportPlaceholder() {
|
||||
dgst := "sha256:mydigest"
|
||||
uuid := `7f20b1b9-6117-4a2e-820b-e4cc0401f15e`
|
||||
scannerUUID := `7f20b1b9-6117-4a2e-820b-e4cc0401f15f`
|
||||
rpt := &scan.Report{
|
||||
UUID: uuid,
|
||||
RegistrationUUID: scannerUUID,
|
||||
Digest: dgst,
|
||||
MimeType: v1.MimeTypeDockerArtifact,
|
||||
}
|
||||
ctx := suite.Context()
|
||||
rptID, err := report.Mgr.Create(ctx, rpt)
|
||||
suite.reportIDs = append(suite.reportIDs, rptID)
|
||||
require.NoError(suite.T(), err)
|
||||
jobLogger := &mockjobservice.MockJobLogger{}
|
||||
report, err := getReportPlaceholder(ctx, dgst, scannerUUID, v1.MimeTypeDockerArtifact, jobLogger)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), report)
|
||||
}
|
||||
|
||||
func (suite *JobTestSuite) TestfetchScanReportFromScanner() {
|
||||
vulnRpt := &vuln.Report{
|
||||
GeneratedAt: time.Now().UTC().String(),
|
||||
Scanner: &v1.Scanner{
|
||||
Name: "Trivy",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Severity: vuln.High,
|
||||
}
|
||||
rptContent, err := json.Marshal(vulnRpt)
|
||||
require.NoError(suite.T(), err)
|
||||
rawContent := string(rptContent)
|
||||
ctx := suite.Context()
|
||||
dgst := "sha256:mydigest"
|
||||
uuid := `7f20b1b9-6117-4a2e-820b-e4cc0401f15a`
|
||||
scannerUUID := `7f20b1b9-6117-4a2e-820b-e4cc0401f15b`
|
||||
rpt := &scan.Report{
|
||||
UUID: uuid,
|
||||
RegistrationUUID: scannerUUID,
|
||||
Digest: dgst,
|
||||
MimeType: v1.MimeTypeDockerArtifact,
|
||||
Report: rawContent,
|
||||
}
|
||||
|
||||
ctx = suite.Context()
|
||||
rptID, err := report.Mgr.Create(ctx, rpt)
|
||||
suite.reportIDs = append(suite.reportIDs, rptID)
|
||||
require.NoError(suite.T(), err)
|
||||
client := &v1testing.Client{}
|
||||
client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport).Return(rawContent, nil)
|
||||
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), rawContent, rawRept)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@ import (
|
|||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
)
|
||||
|
||||
var supportedMimeTypes = []string{
|
||||
MimeTypeNativeReport,
|
||||
MimeTypeGenericVulnerabilityReport,
|
||||
MimeTypeSBOMReport,
|
||||
}
|
||||
|
||||
// Scanner represents metadata of a Scanner Adapter which allow Harbor to lookup a scanner capable of
|
||||
// scanning a given Artifact stored in its registry and making sure that it can interpret a
|
||||
// returned result.
|
||||
|
@ -98,7 +104,7 @@ func (md *ScannerAdapterMetadata) Validate() error {
|
|||
// either of v1.MimeTypeNativeReport OR v1.MimeTypeGenericVulnerabilityReport is required
|
||||
found = false
|
||||
for _, pm := range ca.ProducesMimeTypes {
|
||||
if pm == MimeTypeNativeReport || pm == MimeTypeGenericVulnerabilityReport {
|
||||
if isSupportedMimeType(pm) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
@ -112,6 +118,15 @@ func (md *ScannerAdapterMetadata) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isSupportedMimeType(mimeType string) bool {
|
||||
for _, mt := range supportedMimeTypes {
|
||||
if mt == mimeType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasCapability returns true when mine type of the artifact support by the scanner
|
||||
func (md *ScannerAdapterMetadata) HasCapability(mimeType string) bool {
|
||||
for _, capability := range md.Capabilities {
|
||||
|
@ -173,6 +188,18 @@ type ScanRequest struct {
|
|||
Registry *Registry `json:"registry"`
|
||||
// Artifact to be scanned.
|
||||
Artifact *Artifact `json:"artifact"`
|
||||
// RequestType
|
||||
RequestType []*ScanType `json:"enabled_capabilities"`
|
||||
}
|
||||
|
||||
// ScanType represent the type of the scan request
|
||||
type ScanType struct {
|
||||
// Type sets the type of the scan, it could be sbom or vulnerability, default is vulnerability
|
||||
Type string `json:"type"`
|
||||
// ProducesMimeTypes defines scanreport should be
|
||||
ProducesMimeTypes []string `json:"produces_mime_types"`
|
||||
// Parameters extra parameters
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
// FromJSON parses ScanRequest from json data
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSupportedMimeType(t *testing.T) {
|
||||
// Test with a supported mime type
|
||||
assert.True(t, isSupportedMimeType(MimeTypeSBOMReport), "isSupportedMimeType should return true for supported mime types")
|
||||
|
||||
// Test with an unsupported mime type
|
||||
assert.False(t, isSupportedMimeType("unsupported/mime-type"), "isSupportedMimeType should return false for unsupported mime types")
|
||||
}
|
|
@ -39,9 +39,14 @@ const (
|
|||
MimeTypeScanRequest = "application/vnd.scanner.adapter.scan.request+json; version=1.0"
|
||||
// MimeTypeScanResponse defines the mime type for scan response
|
||||
MimeTypeScanResponse = "application/vnd.scanner.adapter.scan.response+json; version=1.0"
|
||||
// MimeTypeSBOMReport
|
||||
MimeTypeSBOMReport = "application/vnd.security.sbom.report+json; version=1.0"
|
||||
// MimeTypeGenericVulnerabilityReport defines the MIME type for the generic report with enhanced information
|
||||
MimeTypeGenericVulnerabilityReport = "application/vnd.security.vulnerability.report; version=1.1"
|
||||
|
||||
ScanTypeVulnerability = "vulnerability"
|
||||
ScanTypeSbom = "sbom"
|
||||
|
||||
apiPrefix = "/api/v1"
|
||||
)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import (
|
|||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
|
@ -49,7 +49,7 @@ type referrer struct {
|
|||
}
|
||||
|
||||
// GenAccessoryArt composes the accessory oci object and push it back to harbor core as an accessory of the scanned artifact.
|
||||
func GenAccessoryArt(sq v1sq.ScanRequest, accData []byte, accAnnotations map[string]string, mediaType string, robot robot.Robot) (string, error) {
|
||||
func GenAccessoryArt(sq v1sq.ScanRequest, accData []byte, accAnnotations map[string]string, mediaType string, robot *model.Robot) (string, error) {
|
||||
accArt, err := mutate.Append(empty.Image, mutate.Addendum{
|
||||
Layer: static.NewLayer(accData, ocispec.MediaTypeImageLayer),
|
||||
History: v1.History{
|
||||
|
|
|
@ -22,8 +22,7 @@ import (
|
|||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
rm "github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
|
@ -47,11 +46,9 @@ func TestGenAccessoryArt(t *testing.T) {
|
|||
Digest: "sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7",
|
||||
},
|
||||
}
|
||||
r := robot.Robot{
|
||||
Robot: rm.Robot{
|
||||
Name: "admin",
|
||||
Secret: "Harbor12345",
|
||||
},
|
||||
r := &model.Robot{
|
||||
Name: "admin",
|
||||
Secret: "Harbor12345",
|
||||
}
|
||||
|
||||
annotations := map[string]string{
|
||||
|
|
|
@ -33,6 +33,9 @@ type Report struct {
|
|||
Vulnerabilities []*VulnerabilityItem `json:"vulnerabilities"`
|
||||
|
||||
vulnerabilityItemList *VulnerabilityItemList
|
||||
|
||||
// SBOM sbom content
|
||||
SBOM map[string]interface{} `json:"sbom,omitempty"`
|
||||
}
|
||||
|
||||
// GetVulnerabilityItemList returns VulnerabilityItemList from the Vulnerabilities of report
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// 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 vulnerability
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
scanJob "github.com/goharbor/harbor/src/pkg/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
scanJob.RegisterScanHanlder(v1.ScanTypeVulnerability, &ScanHandler{})
|
||||
}
|
||||
|
||||
// ScanHandler defines the handler for scan vulnerability
|
||||
type ScanHandler struct {
|
||||
}
|
||||
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
func (v *ScanHandler) RequiredPermissions() []*types.Policy {
|
||||
return []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PostScan ...
|
||||
func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) {
|
||||
// use a new ormer here to use the short db connection
|
||||
_, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID, origRp.RegistrationUUID, origRp.Digest, rawReport)
|
||||
return refreshedReport, err
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package vulnerability
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/testing/jobservice"
|
||||
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
||||
)
|
||||
|
||||
func TestRequiredPermissions(t *testing.T) {
|
||||
v := &ScanHandler{}
|
||||
expected := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}
|
||||
|
||||
result := v.RequiredPermissions()
|
||||
|
||||
assert.Equal(t, expected, result, "RequiredPermissions should return correct permissions")
|
||||
}
|
||||
|
||||
func TestPostScan(t *testing.T) {
|
||||
v := &ScanHandler{}
|
||||
ctx := &jobservice.MockJobContext{}
|
||||
artifact := &v1.Artifact{}
|
||||
origRp := &scan.Report{}
|
||||
rawReport := ""
|
||||
|
||||
mocker := &postprocessorstesting.ScanReportV1ToV2Converter{}
|
||||
mocker.On("ToRelationalSchema", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, "original report", nil)
|
||||
postprocessors.Converter = mocker
|
||||
sr := &v1.ScanRequest{Artifact: artifact}
|
||||
refreshedReport, err := v.PostScan(ctx, sr, origRp, rawReport, time.Now(), &model.Robot{})
|
||||
assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report")
|
||||
assert.Nil(t, err, "PostScan should not return an error")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package scheduler
|
||||
|
||||
|
@ -18,6 +18,10 @@ type mockDAO struct {
|
|||
func (_m *mockDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
|
||||
|
@ -42,6 +46,10 @@ func (_m *mockDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
|||
func (_m *mockDAO) Create(ctx context.Context, s *schedule) (int64, error) {
|
||||
ret := _m.Called(ctx, s)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *schedule) (int64, error)); ok {
|
||||
|
@ -66,6 +74,10 @@ func (_m *mockDAO) Create(ctx context.Context, s *schedule) (int64, error) {
|
|||
func (_m *mockDAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
|
@ -80,6 +92,10 @@ func (_m *mockDAO) Delete(ctx context.Context, id int64) error {
|
|||
func (_m *mockDAO) Get(ctx context.Context, id int64) (*schedule, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 *schedule
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*schedule, error)); ok {
|
||||
|
@ -106,6 +122,10 @@ func (_m *mockDAO) Get(ctx context.Context, id int64) (*schedule, error) {
|
|||
func (_m *mockDAO) List(ctx context.Context, query *q.Query) ([]*schedule, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []*schedule
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*schedule, error)); ok {
|
||||
|
@ -139,6 +159,10 @@ func (_m *mockDAO) Update(ctx context.Context, s *schedule, props ...string) err
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *schedule, ...string) error); ok {
|
||||
r0 = rf(ctx, s, props...)
|
||||
|
@ -153,6 +177,10 @@ func (_m *mockDAO) Update(ctx context.Context, s *schedule, props ...string) err
|
|||
func (_m *mockDAO) UpdateRevision(ctx context.Context, id int64, revision int64) (int64, error) {
|
||||
ret := _m.Called(ctx, id, revision)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateRevision")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (int64, error)); ok {
|
||||
|
|
|
@ -343,6 +343,12 @@ func (e *executionDAO) refreshStatus(ctx context.Context, id int64) (bool, strin
|
|||
return status != execution.Status, status, false, err
|
||||
}
|
||||
|
||||
type jsonbStru struct {
|
||||
keyPrefix string
|
||||
key string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.QuerySeter, error) {
|
||||
qs, err := orm.QuerySetter(ctx, &Execution{}, query)
|
||||
if err != nil {
|
||||
|
@ -352,39 +358,32 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
|||
// append the filter for "extra attrs"
|
||||
if query != nil && len(query.Keywords) > 0 {
|
||||
var (
|
||||
key string
|
||||
keyPrefix string
|
||||
value interface{}
|
||||
jsonbStrus []jsonbStru
|
||||
args []interface{}
|
||||
)
|
||||
for key, value = range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") {
|
||||
keyPrefix = "ExtraAttrs."
|
||||
break
|
||||
|
||||
for key, value := range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") && key != "ExtraAttrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "ExtraAttrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
if strings.HasPrefix(key, "extra_attrs.") {
|
||||
keyPrefix = "extra_attrs."
|
||||
break
|
||||
if strings.HasPrefix(key, "extra_attrs.") && key != "extra_attrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(keyPrefix) == 0 || keyPrefix == key {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
// key = extra_attrs.artifact.digest
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
// key = extra_attrs.a.b.c
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
keys := strings.Split(strings.TrimPrefix(key, keyPrefix), ".")
|
||||
var args []interface{}
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, value)
|
||||
inClause, err := orm.CreateInClause(ctx, buildInClauseSQLForExtraAttrs(keys), args...)
|
||||
idSQL, args := buildInClauseSQLForExtraAttrs(jsonbStrus)
|
||||
inClause, err := orm.CreateInClause(ctx, idSQL, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -395,23 +394,60 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
|||
}
|
||||
|
||||
// Param keys is strings.Split() after trim "extra_attrs."/"ExtraAttrs." prefix
|
||||
func buildInClauseSQLForExtraAttrs(keys []string) string {
|
||||
switch len(keys) {
|
||||
case 0:
|
||||
// won't fall into this case, as the if condition on "keyPrefix == key"
|
||||
// act as a place holder to ensure "default" is equivalent to "len(keys) >= 2"
|
||||
return ""
|
||||
case 1:
|
||||
return "select id from execution where extra_attrs->>?=?"
|
||||
default:
|
||||
// len(keys) >= 2
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
return fmt.Sprintf("select id from execution where extra_attrs->%s->>?=?", s)
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
//
|
||||
// key = extra_attrs.artifact.digest
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
//
|
||||
// key = extra_attrs.a.b.c
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []interface{}) {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var cond string
|
||||
var args []interface{}
|
||||
sql := "select id from execution where"
|
||||
|
||||
for i, jsonbStr := range jsonbStrus {
|
||||
if jsonbStr.key == "" || jsonbStr.value == "" {
|
||||
return "", nil
|
||||
}
|
||||
keys := strings.Split(strings.TrimPrefix(jsonbStr.key, jsonbStr.keyPrefix), ".")
|
||||
if len(keys) == 1 {
|
||||
if i == 0 {
|
||||
cond += "extra_attrs->>?=?"
|
||||
} else {
|
||||
cond += " and extra_attrs->>?=?"
|
||||
}
|
||||
}
|
||||
if len(keys) >= 2 {
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
if i == 0 {
|
||||
cond += fmt.Sprintf("extra_attrs->%s->>?=?", s)
|
||||
} else {
|
||||
cond += fmt.Sprintf(" and extra_attrs->%s->>?=?", s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, jsonbStr.value)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", sql, cond), args
|
||||
}
|
||||
|
||||
func buildExecStatusOutdateKey(id int64, vendor string) string {
|
||||
|
|
|
@ -395,22 +395,36 @@ func TestExecutionDAOSuite(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_buildInClauseSQLForExtraAttrs(t *testing.T) {
|
||||
type args struct {
|
||||
keys []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
args []jsonbStru
|
||||
want string
|
||||
}{
|
||||
{"extra_attrs.", args{[]string{}}, ""},
|
||||
{"extra_attrs.id", args{[]string{"id"}}, "select id from execution where extra_attrs->>?=?"},
|
||||
{"extra_attrs.artifact.digest", args{[]string{"artifact", "digest"}}, "select id from execution where extra_attrs->?->>?=?"},
|
||||
{"extra_attrs.a.b.c", args{[]string{"a", "b", "c"}}, "select id from execution where extra_attrs->?->?->>?=?"},
|
||||
{"extra_attrs.", []jsonbStru{}, ""},
|
||||
{"extra_attrs.", []jsonbStru{{}}, ""},
|
||||
{"extra_attrs.id", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.id",
|
||||
value: "1",
|
||||
}}, "select id from execution where extra_attrs->>?=?"},
|
||||
{"extra_attrs.artifact.digest", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.artifact.digest",
|
||||
value: "sha256:1234",
|
||||
}}, "select id from execution where extra_attrs->?->>?=?"},
|
||||
{"extra_attrs.a.b.c", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.a.b.c",
|
||||
value: "test_value_1",
|
||||
}, {
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.d.e",
|
||||
value: "test_value_2",
|
||||
}}, "select id from execution where extra_attrs->?->?->>?=? and extra_attrs->?->>?=?"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := buildInClauseSQLForExtraAttrs(tt.args.keys); got != tt.want {
|
||||
if got, _ := buildInClauseSQLForExtraAttrs(tt.args); got != tt.want {
|
||||
t.Errorf("buildInClauseSQLForExtraAttrs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package task
|
||||
|
||||
|
@ -20,6 +20,10 @@ type mockExecutionDAO struct {
|
|||
func (_m *mockExecutionDAO) AsyncRefreshStatus(ctx context.Context, id int64, vendor string) error {
|
||||
ret := _m.Called(ctx, id, vendor)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AsyncRefreshStatus")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok {
|
||||
r0 = rf(ctx, id, vendor)
|
||||
|
@ -34,6 +38,10 @@ func (_m *mockExecutionDAO) AsyncRefreshStatus(ctx context.Context, id int64, ve
|
|||
func (_m *mockExecutionDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
|
||||
|
@ -58,6 +66,10 @@ func (_m *mockExecutionDAO) Count(ctx context.Context, query *q.Query) (int64, e
|
|||
func (_m *mockExecutionDAO) Create(ctx context.Context, execution *dao.Execution) (int64, error) {
|
||||
ret := _m.Called(ctx, execution)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Execution) (int64, error)); ok {
|
||||
|
@ -82,6 +94,10 @@ func (_m *mockExecutionDAO) Create(ctx context.Context, execution *dao.Execution
|
|||
func (_m *mockExecutionDAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
|
@ -96,6 +112,10 @@ func (_m *mockExecutionDAO) Delete(ctx context.Context, id int64) error {
|
|||
func (_m *mockExecutionDAO) Get(ctx context.Context, id int64) (*dao.Execution, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 *dao.Execution
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*dao.Execution, error)); ok {
|
||||
|
@ -122,6 +142,10 @@ func (_m *mockExecutionDAO) Get(ctx context.Context, id int64) (*dao.Execution,
|
|||
func (_m *mockExecutionDAO) GetMetrics(ctx context.Context, id int64) (*dao.Metrics, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetMetrics")
|
||||
}
|
||||
|
||||
var r0 *dao.Metrics
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*dao.Metrics, error)); ok {
|
||||
|
@ -148,6 +172,10 @@ func (_m *mockExecutionDAO) GetMetrics(ctx context.Context, id int64) (*dao.Metr
|
|||
func (_m *mockExecutionDAO) List(ctx context.Context, query *q.Query) ([]*dao.Execution, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []*dao.Execution
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*dao.Execution, error)); ok {
|
||||
|
@ -174,6 +202,10 @@ func (_m *mockExecutionDAO) List(ctx context.Context, query *q.Query) ([]*dao.Ex
|
|||
func (_m *mockExecutionDAO) RefreshStatus(ctx context.Context, id int64) (bool, string, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RefreshStatus")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 string
|
||||
var r2 error
|
||||
|
@ -212,6 +244,10 @@ func (_m *mockExecutionDAO) Update(ctx context.Context, execution *dao.Execution
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Execution, ...string) error); ok {
|
||||
r0 = rf(ctx, execution, props...)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package task
|
||||
|
||||
|
@ -18,6 +18,10 @@ type mockJobserviceClient struct {
|
|||
func (_m *mockJobserviceClient) GetExecutions(uuid string) ([]job.Stats, error) {
|
||||
ret := _m.Called(uuid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetExecutions")
|
||||
}
|
||||
|
||||
var r0 []job.Stats
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]job.Stats, error)); ok {
|
||||
|
@ -44,6 +48,10 @@ func (_m *mockJobserviceClient) GetExecutions(uuid string) ([]job.Stats, error)
|
|||
func (_m *mockJobserviceClient) GetJobLog(uuid string) ([]byte, error) {
|
||||
ret := _m.Called(uuid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJobLog")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]byte, error)); ok {
|
||||
|
@ -70,6 +78,10 @@ func (_m *mockJobserviceClient) GetJobLog(uuid string) ([]byte, error) {
|
|||
func (_m *mockJobserviceClient) GetJobServiceConfig() (*job.Config, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJobServiceConfig")
|
||||
}
|
||||
|
||||
var r0 *job.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (*job.Config, error)); ok {
|
||||
|
@ -96,6 +108,10 @@ func (_m *mockJobserviceClient) GetJobServiceConfig() (*job.Config, error) {
|
|||
func (_m *mockJobserviceClient) PostAction(uuid string, action string) error {
|
||||
ret := _m.Called(uuid, action)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PostAction")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(uuid, action)
|
||||
|
@ -110,6 +126,10 @@ func (_m *mockJobserviceClient) PostAction(uuid string, action string) error {
|
|||
func (_m *mockJobserviceClient) SubmitJob(_a0 *models.JobData) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SubmitJob")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*models.JobData) (string, error)); ok {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package task
|
||||
|
||||
|
@ -17,6 +17,10 @@ type mockSweepManager struct {
|
|||
func (_m *mockSweepManager) Clean(ctx context.Context, execID []int64) error {
|
||||
ret := _m.Called(ctx, execID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Clean")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []int64) error); ok {
|
||||
r0 = rf(ctx, execID)
|
||||
|
@ -31,6 +35,10 @@ func (_m *mockSweepManager) Clean(ctx context.Context, execID []int64) error {
|
|||
func (_m *mockSweepManager) FixDanglingStateExecution(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FixDanglingStateExecution")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
|
@ -45,6 +53,10 @@ func (_m *mockSweepManager) FixDanglingStateExecution(ctx context.Context) error
|
|||
func (_m *mockSweepManager) ListCandidates(ctx context.Context, vendorType string, retainCnt int64) ([]int64, error) {
|
||||
ret := _m.Called(ctx, vendorType, retainCnt)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListCandidates")
|
||||
}
|
||||
|
||||
var r0 []int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]int64, error)); ok {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package task
|
||||
|
||||
|
@ -22,6 +22,10 @@ type mockTaskDAO struct {
|
|||
func (_m *mockTaskDAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
|
||||
|
@ -46,6 +50,10 @@ func (_m *mockTaskDAO) Count(ctx context.Context, query *q.Query) (int64, error)
|
|||
func (_m *mockTaskDAO) Create(ctx context.Context, _a1 *dao.Task) (int64, error) {
|
||||
ret := _m.Called(ctx, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Task) (int64, error)); ok {
|
||||
|
@ -70,6 +78,10 @@ func (_m *mockTaskDAO) Create(ctx context.Context, _a1 *dao.Task) (int64, error)
|
|||
func (_m *mockTaskDAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
|
@ -84,6 +96,10 @@ func (_m *mockTaskDAO) Delete(ctx context.Context, id int64) error {
|
|||
func (_m *mockTaskDAO) ExecutionIDsByVendorAndStatus(ctx context.Context, vendorType string, status string) ([]int64, error) {
|
||||
ret := _m.Called(ctx, vendorType, status)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ExecutionIDsByVendorAndStatus")
|
||||
}
|
||||
|
||||
var r0 []int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]int64, error)); ok {
|
||||
|
@ -110,6 +126,10 @@ func (_m *mockTaskDAO) ExecutionIDsByVendorAndStatus(ctx context.Context, vendor
|
|||
func (_m *mockTaskDAO) Get(ctx context.Context, id int64) (*dao.Task, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 *dao.Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*dao.Task, error)); ok {
|
||||
|
@ -136,6 +156,10 @@ func (_m *mockTaskDAO) Get(ctx context.Context, id int64) (*dao.Task, error) {
|
|||
func (_m *mockTaskDAO) GetMaxEndTime(ctx context.Context, executionID int64) (time.Time, error) {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetMaxEndTime")
|
||||
}
|
||||
|
||||
var r0 time.Time
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (time.Time, error)); ok {
|
||||
|
@ -160,6 +184,10 @@ func (_m *mockTaskDAO) GetMaxEndTime(ctx context.Context, executionID int64) (ti
|
|||
func (_m *mockTaskDAO) List(ctx context.Context, query *q.Query) ([]*dao.Task, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []*dao.Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*dao.Task, error)); ok {
|
||||
|
@ -186,6 +214,10 @@ func (_m *mockTaskDAO) List(ctx context.Context, query *q.Query) ([]*dao.Task, e
|
|||
func (_m *mockTaskDAO) ListScanTasksByReportUUID(ctx context.Context, uuid string) ([]*dao.Task, error) {
|
||||
ret := _m.Called(ctx, uuid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListScanTasksByReportUUID")
|
||||
}
|
||||
|
||||
var r0 []*dao.Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*dao.Task, error)); ok {
|
||||
|
@ -212,6 +244,10 @@ func (_m *mockTaskDAO) ListScanTasksByReportUUID(ctx context.Context, uuid strin
|
|||
func (_m *mockTaskDAO) ListStatusCount(ctx context.Context, executionID int64) ([]*dao.StatusCount, error) {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListStatusCount")
|
||||
}
|
||||
|
||||
var r0 []*dao.StatusCount
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*dao.StatusCount, error)); ok {
|
||||
|
@ -245,6 +281,10 @@ func (_m *mockTaskDAO) Update(ctx context.Context, _a1 *dao.Task, props ...strin
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Task, ...string) error); ok {
|
||||
r0 = rf(ctx, _a1, props...)
|
||||
|
@ -259,6 +299,10 @@ func (_m *mockTaskDAO) Update(ctx context.Context, _a1 *dao.Task, props ...strin
|
|||
func (_m *mockTaskDAO) UpdateStatus(ctx context.Context, id int64, status string, statusRevision int64) error {
|
||||
ret := _m.Called(ctx, id, status, statusRevision)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateStatus")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string, int64) error); ok {
|
||||
r0 = rf(ctx, id, status, statusRevision)
|
||||
|
@ -273,6 +317,10 @@ func (_m *mockTaskDAO) UpdateStatus(ctx context.Context, id int64, status string
|
|||
func (_m *mockTaskDAO) UpdateStatusInBatch(ctx context.Context, jobIDs []string, status string, batchSize int) error {
|
||||
ret := _m.Called(ctx, jobIDs, status, batchSize)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateStatusInBatch")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []string, string, int) error); ok {
|
||||
r0 = rf(ctx, jobIDs, status, batchSize)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.35.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.2. DO NOT EDIT.
|
||||
|
||||
package task
|
||||
|
||||
|
@ -18,6 +18,10 @@ type mockTaskManager struct {
|
|||
func (_m *mockTaskManager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) (int64, error)); ok {
|
||||
|
@ -49,6 +53,10 @@ func (_m *mockTaskManager) Create(ctx context.Context, executionID int64, job *J
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *Job, ...map[string]interface{}) (int64, error)); ok {
|
||||
|
@ -73,6 +81,10 @@ func (_m *mockTaskManager) Create(ctx context.Context, executionID int64, job *J
|
|||
func (_m *mockTaskManager) ExecutionIDsByVendorAndStatus(ctx context.Context, vendorType string, status string) ([]int64, error) {
|
||||
ret := _m.Called(ctx, vendorType, status)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ExecutionIDsByVendorAndStatus")
|
||||
}
|
||||
|
||||
var r0 []int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]int64, error)); ok {
|
||||
|
@ -99,6 +111,10 @@ func (_m *mockTaskManager) ExecutionIDsByVendorAndStatus(ctx context.Context, ve
|
|||
func (_m *mockTaskManager) Get(ctx context.Context, id int64) (*Task, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 *Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*Task, error)); ok {
|
||||
|
@ -125,6 +141,10 @@ func (_m *mockTaskManager) Get(ctx context.Context, id int64) (*Task, error) {
|
|||
func (_m *mockTaskManager) GetLog(ctx context.Context, id int64) ([]byte, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLog")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]byte, error)); ok {
|
||||
|
@ -151,6 +171,10 @@ func (_m *mockTaskManager) GetLog(ctx context.Context, id int64) ([]byte, error)
|
|||
func (_m *mockTaskManager) GetLogByJobID(ctx context.Context, jobID string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, jobID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLogByJobID")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
|
||||
|
@ -177,6 +201,10 @@ func (_m *mockTaskManager) GetLogByJobID(ctx context.Context, jobID string) ([]b
|
|||
func (_m *mockTaskManager) List(ctx context.Context, query *q.Query) ([]*Task, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []*Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*Task, error)); ok {
|
||||
|
@ -203,6 +231,10 @@ func (_m *mockTaskManager) List(ctx context.Context, query *q.Query) ([]*Task, e
|
|||
func (_m *mockTaskManager) ListScanTasksByReportUUID(ctx context.Context, uuid string) ([]*Task, error) {
|
||||
ret := _m.Called(ctx, uuid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListScanTasksByReportUUID")
|
||||
}
|
||||
|
||||
var r0 []*Task
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*Task, error)); ok {
|
||||
|
@ -229,6 +261,10 @@ func (_m *mockTaskManager) ListScanTasksByReportUUID(ctx context.Context, uuid s
|
|||
func (_m *mockTaskManager) Stop(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Stop")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
|
@ -250,6 +286,10 @@ func (_m *mockTaskManager) Update(ctx context.Context, task *Task, props ...stri
|
|||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *Task, ...string) error); ok {
|
||||
r0 = rf(ctx, task, props...)
|
||||
|
@ -264,6 +304,10 @@ func (_m *mockTaskManager) Update(ctx context.Context, task *Task, props ...stri
|
|||
func (_m *mockTaskManager) UpdateExtraAttrs(ctx context.Context, id int64, extraAttrs map[string]interface{}) error {
|
||||
ret := _m.Called(ctx, id, extraAttrs)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateExtraAttrs")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, map[string]interface{}) error); ok {
|
||||
r0 = rf(ctx, id, extraAttrs)
|
||||
|
@ -278,6 +322,10 @@ func (_m *mockTaskManager) UpdateExtraAttrs(ctx context.Context, id int64, extra
|
|||
func (_m *mockTaskManager) UpdateStatusInBatch(ctx context.Context, jobIDs []string, status string, batchSize int) error {
|
||||
ret := _m.Called(ctx, jobIDs, status, batchSize)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateStatusInBatch")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []string, string, int) error); ok {
|
||||
r0 = rf(ctx, jobIDs, status, batchSize)
|
||||
|
|
|
@ -18,4 +18,5 @@ export enum ADDITIONS {
|
|||
SUMMARY = 'readme.md',
|
||||
VALUES = 'values.yaml',
|
||||
DEPENDENCIES = 'dependencies',
|
||||
SBOMS = 'sboms',
|
||||
}
|
||||
|
|
|
@ -1,12 +1,56 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { ArtifactListPageService } from './artifact-list-page.service';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import {
|
||||
ScanningResultService,
|
||||
UserPermissionService,
|
||||
} from 'src/app/shared/services';
|
||||
import { of } from 'rxjs';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
|
||||
describe('ArtifactListPageService', () => {
|
||||
const FakedScanningResultService = {
|
||||
getProjectScanner: () =>
|
||||
of({
|
||||
access_credential: '',
|
||||
adapter: 'Trivy',
|
||||
auth: '',
|
||||
capabilities: {
|
||||
support_sbom: true,
|
||||
support_vulnerability: true,
|
||||
},
|
||||
create_time: '2024-03-06T09:29:43.789Z',
|
||||
description: 'The Trivy scanner adapter',
|
||||
disabled: false,
|
||||
health: 'healthy',
|
||||
is_default: true,
|
||||
name: 'Trivy',
|
||||
skip_certVerify: false,
|
||||
update_time: '2024-03-06T09:29:43.789Z',
|
||||
url: 'http://trivy-adapter:8080',
|
||||
use_internal_addr: true,
|
||||
uuid: '10c68b62-db9c-11ee-9c72-0242ac130009',
|
||||
vendor: 'Aqua Security',
|
||||
version: 'v0.47.0',
|
||||
}),
|
||||
};
|
||||
const FakedUserPermissionService = {
|
||||
hasProjectPermissions: () => of([true, true, true, true, true]),
|
||||
};
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
providers: [ArtifactListPageService],
|
||||
providers: [
|
||||
ArtifactListPageService,
|
||||
{
|
||||
provide: ScanningResultService,
|
||||
useValue: FakedScanningResultService,
|
||||
},
|
||||
{
|
||||
provide: UserPermissionService,
|
||||
useValue: FakedUserPermissionService,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -16,4 +60,51 @@ describe('ArtifactListPageService', () => {
|
|||
expect(service).toBeTruthy();
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService Permissions validation ', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.init(3);
|
||||
expect(service.hasSbomPermission()).toBeTruthy();
|
||||
expect(service.hasAddLabelImagePermission()).toBeTruthy();
|
||||
expect(service.hasRetagImagePermission()).toBeTruthy();
|
||||
expect(service.hasDeleteImagePermission()).toBeTruthy();
|
||||
expect(service.hasScanImagePermission()).toBeTruthy();
|
||||
expect(service.hasScannerSupportVulnerability()).toBeTruthy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeTruthy();
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService updateStates', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.init(3);
|
||||
expect(service.hasEnabledScanner()).toBeTruthy();
|
||||
expect(service.getScanBtnState()).toBe(ClrLoadingState.SUCCESS);
|
||||
expect(service.getSbomBtnState()).toBe(ClrLoadingState.SUCCESS);
|
||||
service.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
expect(service.hasEnabledScanner()).toBeFalsy();
|
||||
expect(service.getScanBtnState()).toBe(ClrLoadingState.ERROR);
|
||||
expect(service.getSbomBtnState()).toBe(ClrLoadingState.ERROR);
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService updateCapabilities ', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.updateCapabilities({
|
||||
support_vulnerability: true,
|
||||
support_sbom: true,
|
||||
});
|
||||
expect(service.hasScannerSupportVulnerability()).toBeTruthy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeTruthy();
|
||||
service.updateCapabilities({
|
||||
support_vulnerability: false,
|
||||
support_sbom: false,
|
||||
});
|
||||
expect(service.hasScannerSupportVulnerability()).toBeFalsy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeFalsy();
|
||||
}
|
||||
));
|
||||
});
|
||||
|
|
|
@ -10,11 +10,15 @@ import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
|||
@Injectable()
|
||||
export class ArtifactListPageService {
|
||||
private _scanBtnState: ClrLoadingState;
|
||||
private _sbomBtnState: ClrLoadingState;
|
||||
private _hasEnabledScanner: boolean = false;
|
||||
private _hasScannerSupportVulnerability: boolean = false;
|
||||
private _hasScannerSupportSBOM: boolean = false;
|
||||
private _hasAddLabelImagePermission: boolean = false;
|
||||
private _hasRetagImagePermission: boolean = false;
|
||||
private _hasDeleteImagePermission: boolean = false;
|
||||
private _hasScanImagePermission: boolean = false;
|
||||
private _hasSbomPermission: boolean = false;
|
||||
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
|
@ -26,6 +30,10 @@ export class ArtifactListPageService {
|
|||
return this._scanBtnState;
|
||||
}
|
||||
|
||||
getSbomBtnState(): ClrLoadingState {
|
||||
return this._sbomBtnState;
|
||||
}
|
||||
|
||||
hasEnabledScanner(): boolean {
|
||||
return this._hasEnabledScanner;
|
||||
}
|
||||
|
@ -46,14 +54,53 @@ export class ArtifactListPageService {
|
|||
return this._hasScanImagePermission;
|
||||
}
|
||||
|
||||
hasSbomPermission(): boolean {
|
||||
return this._hasSbomPermission;
|
||||
}
|
||||
|
||||
hasScannerSupportVulnerability(): boolean {
|
||||
return this._hasScannerSupportVulnerability;
|
||||
}
|
||||
|
||||
hasScannerSupportSBOM(): boolean {
|
||||
return this._hasScannerSupportSBOM;
|
||||
}
|
||||
|
||||
init(projectId: number) {
|
||||
this._getProjectScanner(projectId);
|
||||
this._getPermissionRule(projectId);
|
||||
}
|
||||
|
||||
updateStates(
|
||||
enabledScanner: boolean,
|
||||
scanState?: ClrLoadingState,
|
||||
sbomState?: ClrLoadingState
|
||||
) {
|
||||
if (scanState) {
|
||||
this._scanBtnState = scanState;
|
||||
}
|
||||
if (sbomState) {
|
||||
this._sbomBtnState = sbomState;
|
||||
}
|
||||
this._hasEnabledScanner = enabledScanner;
|
||||
}
|
||||
|
||||
updateCapabilities(capabilities?: any) {
|
||||
if (capabilities) {
|
||||
if (capabilities?.support_vulnerability !== undefined) {
|
||||
this._hasScannerSupportVulnerability =
|
||||
capabilities.support_vulnerability;
|
||||
}
|
||||
if (capabilities?.support_sbom !== undefined) {
|
||||
this._hasScannerSupportSBOM = capabilities.support_sbom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getProjectScanner(projectId: number): void {
|
||||
this._hasEnabledScanner = false;
|
||||
this._scanBtnState = ClrLoadingState.LOADING;
|
||||
this._sbomBtnState = ClrLoadingState.LOADING;
|
||||
this.scanningService.getProjectScanner(projectId).subscribe(
|
||||
response => {
|
||||
if (
|
||||
|
@ -62,14 +109,28 @@ export class ArtifactListPageService {
|
|||
!response.disabled &&
|
||||
response.health === 'healthy'
|
||||
) {
|
||||
this._scanBtnState = ClrLoadingState.SUCCESS;
|
||||
this._hasEnabledScanner = true;
|
||||
this.updateStates(
|
||||
true,
|
||||
ClrLoadingState.SUCCESS,
|
||||
ClrLoadingState.SUCCESS
|
||||
);
|
||||
if (response?.capabilities) {
|
||||
this.updateCapabilities(response?.capabilities);
|
||||
}
|
||||
} else {
|
||||
this._scanBtnState = ClrLoadingState.ERROR;
|
||||
this.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this._scanBtnState = ClrLoadingState.ERROR;
|
||||
this.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -94,6 +155,11 @@ export class ArtifactListPageService {
|
|||
action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE
|
||||
.CREATE,
|
||||
},
|
||||
{
|
||||
resource: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.KEY,
|
||||
action: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.VALUE
|
||||
.CREATE,
|
||||
},
|
||||
];
|
||||
this.userPermissionService
|
||||
.hasProjectPermissions(projectId, permissions)
|
||||
|
@ -103,6 +169,9 @@ export class ArtifactListPageService {
|
|||
this._hasRetagImagePermission = results[1];
|
||||
this._hasDeleteImagePermission = results[2];
|
||||
this._hasScanImagePermission = results[3];
|
||||
this._hasSbomPermission = results?.[4] ?? false;
|
||||
// TODO need to remove the static code
|
||||
this._hasSbomPermission = true;
|
||||
},
|
||||
error => this.errorHandlerService.error(error)
|
||||
);
|
||||
|
|
|
@ -42,6 +42,34 @@
|
|||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'VULNERABILITY.STOP_NOW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasEnabledSbom()"
|
||||
id="generate-sbom-btn"
|
||||
[clrLoading]="generateSbomBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="true"
|
||||
(click)="generateSbom()">
|
||||
<clr-icon shape="file" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
id="stop-sbom-btn"
|
||||
*ngIf="hasEnabledSbom()"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn"
|
||||
[disabled]="
|
||||
!(
|
||||
canStopSbom() &&
|
||||
hasSbomPermission &&
|
||||
hasEnabledScanner
|
||||
)
|
||||
"
|
||||
(click)="stopSbom()">
|
||||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.STOP' | translate }}</span>
|
||||
</button>
|
||||
<clr-dropdown class="btn btn-link" *ngIf="!depth">
|
||||
<span
|
||||
clrDropdownTrigger
|
||||
|
@ -174,23 +202,29 @@
|
|||
{{ 'REPOSITORY.VULNERABILITY' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="annotations-column">
|
||||
<clr-dg-column class="sbom-column">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[6] }">
|
||||
{{ 'REPOSITORY.SBOM' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="annotations-column">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[7] }">
|
||||
{{ 'ARTIFACT.ANNOTATION' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[7] }">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[8] }">
|
||||
{{ 'REPOSITORY.LABELS' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pushComparator">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[8] }">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[9] }">
|
||||
{{ 'REPOSITORY.PUSH_TIME' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pullComparator">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[9] }">
|
||||
<ng-template
|
||||
[clrDgHideableColumn]="{ hidden: hiddenArray[10] }">
|
||||
{{ 'REPOSITORY.PULL_TIME' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
|
@ -389,6 +423,26 @@
|
|||
</hbr-vulnerability-bar>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div class="cell">
|
||||
<span *ngIf="!hasScannerSupportSBOM">
|
||||
{{ 'ARTIFACT.SBOM_UNSUPPORTED' | translate }}
|
||||
</span>
|
||||
<hbr-sbom-bar
|
||||
(submitStopFinish)="submitSbomStopFinish($event)"
|
||||
(scanFinished)="sbomFinished($event)"
|
||||
*ngIf="hasScannerSupportSBOM"
|
||||
[inputScanner]="artifact?.sbom_overview?.scanner"
|
||||
(submitFinish)="submitSbomFinish($event)"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[artifactDigest]="artifact?.digest"
|
||||
[sbomDigest]="artifact?.sbomDigest"
|
||||
[sbomOverview]="artifact?.sbom_overview">
|
||||
</hbr-sbom-bar>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div class="cell" *ngIf="artifact.annotationsArray?.length">
|
||||
<div class="bar-state">
|
||||
|
|
|
@ -161,6 +161,10 @@
|
|||
width: 11rem !important;
|
||||
}
|
||||
|
||||
.sbom-column {
|
||||
width: 6rem !important;
|
||||
}
|
||||
|
||||
.annotations-column {
|
||||
width: 5rem !important;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
ScanningResultDefaultService,
|
||||
ScanningResultService,
|
||||
} from '../../../../../../../shared/services';
|
||||
import { ArtifactFront as Artifact } from '../../../artifact';
|
||||
import { ArtifactFront as Artifact, ArtifactFront } from '../../../artifact';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
import { OperationService } from '../../../../../../../shared/components/operation/operation.service';
|
||||
import { ArtifactService as NewArtifactService } from '../../../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
|
@ -24,6 +24,10 @@ import { ArtifactListPageService } from '../../artifact-list-page.service';
|
|||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
import { ArtifactModule } from '../../../artifact.module';
|
||||
import {
|
||||
SBOM_SCAN_STATUS,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
} from 'src/app/shared/units/utils';
|
||||
|
||||
describe('ArtifactListTabComponent', () => {
|
||||
let comp: ArtifactListTabComponent;
|
||||
|
@ -171,6 +175,16 @@ describe('ArtifactListTabComponent', () => {
|
|||
pull_time: '0001-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
const mockAccessory = <Accessory>{
|
||||
id: 1,
|
||||
artifact_id: 2,
|
||||
subject_artifact_id: 3,
|
||||
subject_artifact_digest: 'fakeDigest',
|
||||
subject_artifact_repo: 'test',
|
||||
size: 120,
|
||||
digest: 'fakeDigest',
|
||||
type: 'test',
|
||||
};
|
||||
const mockErrorHandler = {
|
||||
error: () => {},
|
||||
};
|
||||
|
@ -236,9 +250,18 @@ describe('ArtifactListTabComponent', () => {
|
|||
getScanBtnState(): ClrLoadingState {
|
||||
return ClrLoadingState.DEFAULT;
|
||||
},
|
||||
getSbomBtnState(): ClrLoadingState {
|
||||
return ClrLoadingState.DEFAULT;
|
||||
},
|
||||
hasEnabledScanner(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasSbomPermission(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasScannerSupportSBOM(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasAddLabelImagePermission(): boolean {
|
||||
return true;
|
||||
},
|
||||
|
@ -353,6 +376,27 @@ describe('ArtifactListTabComponent', () => {
|
|||
fixture.nativeElement.querySelector('.confirmation-title')
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('Generate SBOM button should be disabled', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
const generatedButton =
|
||||
fixture.nativeElement.querySelector('#generate-sbom-btn');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(generatedButton.disabled).toBeTruthy();
|
||||
});
|
||||
it('Stop SBOM button should be disabled', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
const stopButton =
|
||||
fixture.nativeElement.querySelector('#stop-sbom-btn');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(stopButton.disabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('the length of hide array should equal to the number of column', async () => {
|
||||
comp.loading = false;
|
||||
fixture.detectChanges();
|
||||
|
@ -360,6 +404,93 @@ describe('ArtifactListTabComponent', () => {
|
|||
const cols = fixture.nativeElement.querySelectorAll('.datagrid-column');
|
||||
expect(cols.length).toEqual(comp.hiddenArray.length);
|
||||
});
|
||||
|
||||
it('Test isEllipsisActive', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(
|
||||
comp.isEllipsisActive(document.createElement('span'))
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
it('Test deleteAccessory', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.deleteAccessory(mockAccessory);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.confirmation-content')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('Test scanNow', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.scanNow();
|
||||
expect(comp.onScanArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test stopNow', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.stopNow();
|
||||
expect(comp.onStopScanArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test stopSbom', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.stopSbom();
|
||||
expect(comp.onStopSbomArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test tagsString and isRunningState and canStopSbom and canStopScan', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.tagsString([])).toBeNull();
|
||||
expect(
|
||||
comp.isRunningState(VULNERABILITY_SCAN_STATUS.RUNNING)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
comp.isRunningState(VULNERABILITY_SCAN_STATUS.ERROR)
|
||||
).toBeFalsy();
|
||||
expect(comp.canStopSbom()).toBeFalsy();
|
||||
expect(comp.canStopScan()).toBeFalsy();
|
||||
});
|
||||
it('Test status and handleScanOverview', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.scanStatus(mockArtifacts[0])).toBe(
|
||||
VULNERABILITY_SCAN_STATUS.ERROR
|
||||
);
|
||||
expect(comp.sbomStatus(null)).toBe(SBOM_SCAN_STATUS.NOT_GENERATED_SBOM);
|
||||
expect(comp.sbomStatus(mockArtifacts[0])).toBe(
|
||||
SBOM_SCAN_STATUS.NOT_GENERATED_SBOM
|
||||
);
|
||||
expect(comp.handleScanOverview(mockArtifacts[0])).not.toBeNull();
|
||||
});
|
||||
it('Test utils', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.selectedRowHasSbom()).toBeFalsy();
|
||||
expect(comp.selectedRowHasVul()).toBeFalsy();
|
||||
expect(comp.canScanNow()).toBeFalsy();
|
||||
expect(comp.hasEnabledSbom()).toBeTruthy();
|
||||
expect(comp.canAddLabel()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
async function stepOpenAction(fixture, comp) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
setHiddenArrayToLocalStorage,
|
||||
setPageSizeToLocalStorage,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
SBOM_SCAN_STATUS,
|
||||
} from '../../../../../../../shared/units/utils';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
import { ArtifactService } from '../../../artifact.service';
|
||||
|
@ -76,7 +77,7 @@ import {
|
|||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../../../../../services/event-service/event.service';
|
||||
import { AppConfigService } from 'src/app/services/app-config.service';
|
||||
import { AppConfigService } from '../../../../../../../services/app-config.service';
|
||||
import { ArtifactListPageService } from '../../artifact-list-page.service';
|
||||
import { ACCESSORY_PAGE_SIZE } from './sub-accessories/sub-accessories.component';
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
|
@ -141,28 +142,60 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
get hasScanImagePermission(): boolean {
|
||||
return this.artifactListPageService.hasScanImagePermission();
|
||||
}
|
||||
get hasSbomPermission(): boolean {
|
||||
return this.artifactListPageService.hasSbomPermission();
|
||||
}
|
||||
get hasEnabledScanner(): boolean {
|
||||
return this.artifactListPageService.hasEnabledScanner();
|
||||
}
|
||||
get hasScannerSupportVulnerability(): boolean {
|
||||
return this.artifactListPageService.hasScannerSupportVulnerability();
|
||||
}
|
||||
get hasScannerSupportSBOM(): boolean {
|
||||
return this.artifactListPageService.hasScannerSupportSBOM();
|
||||
}
|
||||
get scanBtnState(): ClrLoadingState {
|
||||
return this.artifactListPageService.getScanBtnState();
|
||||
}
|
||||
get generateSbomBtnState(): ClrLoadingState {
|
||||
return this.artifactListPageService.getSbomBtnState();
|
||||
}
|
||||
onSendingScanCommand: boolean;
|
||||
onSendingStopScanCommand: boolean = false;
|
||||
onStopScanArtifactsLength: number = 0;
|
||||
scanStoppedArtifactLength: number = 0;
|
||||
|
||||
onSendingSbomCommand: boolean;
|
||||
onSendingStopSbomCommand: boolean = false;
|
||||
onStopSbomArtifactsLength: number = 0;
|
||||
sbomStoppedArtifactLength: number = 0;
|
||||
|
||||
artifactDigest: string;
|
||||
depth: string;
|
||||
// could Pagination filter
|
||||
filters: string[];
|
||||
scanFinishedArtifactLength: number = 0;
|
||||
onScanArtifactsLength: number = 0;
|
||||
sbomFinishedArtifactLength: number = 0;
|
||||
onSbomArtifactsLength: number = 0;
|
||||
stopBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
updateArtifactSub: Subscription;
|
||||
|
||||
hiddenArray: boolean[] = getHiddenArrayFromLocalStorage(
|
||||
PageSizeMapKeys.ARTIFACT_LIST_TAB_COMPONENT,
|
||||
[false, false, false, false, false, false, true, false, false, false]
|
||||
[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]
|
||||
);
|
||||
deleteAccessorySub: Subscription;
|
||||
copyDigestSub: Subscription;
|
||||
|
@ -203,7 +236,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
ngOnInit() {
|
||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||
const appConfig = this.appConfigService.getConfig();
|
||||
this.registryUrl = appConfig.registry_url;
|
||||
this.initRouterData();
|
||||
if (!this.updateArtifactSub) {
|
||||
this.updateArtifactSub = this.eventService.subscribe(
|
||||
|
@ -250,7 +284,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
this.copyDigestSub.unsubscribe();
|
||||
this.copyDigestSub = null;
|
||||
}
|
||||
this.datagrid['columnsService']?.columns?.forEach((item, index) => {
|
||||
this.datagrid?.['columnsService']?.columns?.forEach((item, index) => {
|
||||
if (this.depth) {
|
||||
this.hiddenArray[index] = !!item?._value?.hidden;
|
||||
} else {
|
||||
|
@ -326,6 +360,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
withImmutableStatus: true,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
withAccessory: false,
|
||||
|
@ -350,6 +385,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
withImmutableStatus: true,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
XAcceptVulnerabilities:
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
|
@ -381,7 +417,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
this.getArtifactTagsAsync(this.artifactList);
|
||||
this.getAccessoriesAsync(this.artifactList);
|
||||
this.checkCosignAsync(this.artifactList);
|
||||
this.checkCosignAndSbomAsync(this.artifactList);
|
||||
this.getIconsFromBackEnd();
|
||||
},
|
||||
error => {
|
||||
|
@ -420,7 +456,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
this.artifactList = res.body;
|
||||
this.getArtifactTagsAsync(this.artifactList);
|
||||
this.getAccessoriesAsync(this.artifactList);
|
||||
this.checkCosignAsync(this.artifactList);
|
||||
this.checkCosignAndSbomAsync(this.artifactList);
|
||||
this.getIconsFromBackEnd();
|
||||
},
|
||||
error => {
|
||||
|
@ -519,6 +555,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
return formatSize(tagSize);
|
||||
}
|
||||
|
||||
hasEnabledSbom(): boolean {
|
||||
return (
|
||||
this.hasScannerSupportSBOM &&
|
||||
this.hasEnabledScanner &&
|
||||
this.hasSbomPermission
|
||||
);
|
||||
}
|
||||
|
||||
retag() {
|
||||
if (this.selectedRow && this.selectedRow.length && !this.depth) {
|
||||
this.copyArtifactComponent.retag(this.selectedRow[0].digest);
|
||||
|
@ -714,6 +758,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
// Get sbom status
|
||||
sbomStatus(artifact: Artifact): string {
|
||||
if (artifact) {
|
||||
let so = (<any>artifact).sbom_overview;
|
||||
if (so && so.scan_status) {
|
||||
return so.scan_status;
|
||||
}
|
||||
}
|
||||
return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM;
|
||||
}
|
||||
|
||||
// Get vulnerability scanning status
|
||||
scanStatus(artifact: Artifact): string {
|
||||
if (artifact) {
|
||||
|
@ -771,6 +826,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
selectedRowHasSbom(): boolean {
|
||||
return !!(this.selectedRow && this.selectedRow[0]);
|
||||
}
|
||||
|
||||
hasVul(artifact: Artifact): boolean {
|
||||
return !!(
|
||||
artifact &&
|
||||
|
@ -779,6 +838,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
hasSbom(artifact: Artifact): boolean {
|
||||
return !!(
|
||||
artifact &&
|
||||
artifact.addition_links &&
|
||||
artifact.addition_links[ADDITIONS.SBOMS]
|
||||
);
|
||||
}
|
||||
|
||||
submitFinish(e: boolean) {
|
||||
this.scanFinishedArtifactLength += 1;
|
||||
// all selected scan action has started
|
||||
|
@ -794,9 +861,27 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
this.onSendingScanCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
submitSbomFinish(e: boolean) {
|
||||
this.sbomFinishedArtifactLength += 1;
|
||||
// all selected scan action has started
|
||||
if (this.sbomFinishedArtifactLength === this.onSbomArtifactsLength) {
|
||||
this.onSendingSbomCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
submitSbomStopFinish(e: boolean) {
|
||||
this.sbomStoppedArtifactLength += 1;
|
||||
// all selected scan action has stopped
|
||||
if (this.sbomStoppedArtifactLength === this.onStopSbomArtifactsLength) {
|
||||
this.onSendingSbomCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
handleScanOverview(scanOverview: any): any {
|
||||
if (scanOverview) {
|
||||
return Object.values(scanOverview)[0];
|
||||
const keys = Object.keys(scanOverview) ?? [];
|
||||
return keys.length > 0 ? scanOverview[keys[0]] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -857,6 +942,11 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
// when finished, remove it from selectedRow
|
||||
sbomFinished(artifact: Artifact) {
|
||||
this.scanFinished(artifact);
|
||||
}
|
||||
|
||||
getIconsFromBackEnd() {
|
||||
if (this.artifactList && this.artifactList.length) {
|
||||
this.artifactService.getIconsFromBackEnd(this.artifactList);
|
||||
|
@ -929,11 +1019,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
}
|
||||
checkCosignAsync(artifacts: ArtifactFront[]) {
|
||||
|
||||
checkCosignAndSbomAsync(artifacts: ArtifactFront[]) {
|
||||
if (artifacts) {
|
||||
if (artifacts.length) {
|
||||
artifacts.forEach(item => {
|
||||
item.signed = CHECKING;
|
||||
// const sbomOverview = item?.sbom_overview;
|
||||
// item.sbomDigest = sbomOverview?.sbom_digest;
|
||||
// let queryTypes = `${AccessoryType.COSIGN} ${AccessoryType.NOTATION}`;
|
||||
// if (!item.sbomDigest) {
|
||||
// queryTypes = `${queryTypes} ${AccessoryType.SBOM}`;
|
||||
// }
|
||||
this.newArtifactService
|
||||
.listAccessories({
|
||||
projectName: this.projectName,
|
||||
|
@ -979,6 +1076,24 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
// return true if all selected rows are in "running" state
|
||||
canStopSbom(): boolean {
|
||||
if (this.onSendingStopSbomCommand) {
|
||||
return false;
|
||||
}
|
||||
if (this.selectedRow && this.selectedRow.length) {
|
||||
let flag: boolean = true;
|
||||
this.selectedRow.forEach(item => {
|
||||
const st: string = this.sbomStatus(item);
|
||||
if (!this.isRunningState(st)) {
|
||||
flag = false;
|
||||
}
|
||||
});
|
||||
return flag;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isRunningState(state: string): boolean {
|
||||
return (
|
||||
state === VULNERABILITY_SCAN_STATUS.RUNNING ||
|
||||
|
@ -1001,6 +1116,22 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
stopSbom() {
|
||||
if (this.selectedRow && this.selectedRow.length) {
|
||||
this.sbomStoppedArtifactLength = 0;
|
||||
this.onStopSbomArtifactsLength = this.selectedRow.length;
|
||||
this.onSendingStopSbomCommand = true;
|
||||
this.selectedRow.forEach((data: any) => {
|
||||
let digest = data.digest;
|
||||
this.eventService.publish(
|
||||
HarborEvent.STOP_SBOM_ARTIFACT,
|
||||
this.repoName + '/' + digest
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tagsString(tags: Tag[]): string {
|
||||
if (tags?.length) {
|
||||
const arr: string[] = [];
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ArtifactVulnerabilitiesComponent } from './artifact-additions/artifact-
|
|||
import { ArtifactDefaultService, ArtifactService } from './artifact.service';
|
||||
import { ArtifactDetailRoutingResolverService } from '../../../../services/routing-resolvers/artifact-detail-routing-resolver.service';
|
||||
import { ResultBarChartComponent } from './vulnerability-scanning/result-bar-chart.component';
|
||||
import { ResultSbomComponent } from './sbom-scanning/sbom-scan.component';
|
||||
import { ResultTipHistogramComponent } from './vulnerability-scanning/result-tip-histogram/result-tip-histogram.component';
|
||||
import { HistogramChartComponent } from './vulnerability-scanning/histogram-chart/histogram-chart.component';
|
||||
import { ArtifactInfoComponent } from './artifact-list-page/artifact-list/artifact-info/artifact-info.component';
|
||||
|
@ -24,6 +25,7 @@ import { CopyArtifactComponent } from './artifact-list-page/artifact-list/artifa
|
|||
import { CopyDigestComponent } from './artifact-list-page/artifact-list/artifact-list-tab/copy-digest/copy-digest.component';
|
||||
import { ArtifactFilterComponent } from './artifact-list-page/artifact-list/artifact-list-tab/artifact-filter/artifact-filter.component';
|
||||
import { PullCommandComponent } from './artifact-list-page/artifact-list/artifact-list-tab/pull-command/pull-command.component';
|
||||
import { SbomTipHistogramComponent } from './sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -80,6 +82,8 @@ const routes: Routes = [
|
|||
BuildHistoryComponent,
|
||||
ArtifactVulnerabilitiesComponent,
|
||||
ResultBarChartComponent,
|
||||
ResultSbomComponent,
|
||||
SbomTipHistogramComponent,
|
||||
ResultTipHistogramComponent,
|
||||
HistogramChartComponent,
|
||||
ArtifactInfoComponent,
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/* tslint:disable */
|
||||
|
||||
import { Scanner } from 'ng-swagger-gen/models';
|
||||
|
||||
/**
|
||||
* The generate SBOM overview information
|
||||
*/
|
||||
export interface SBOMOverview {
|
||||
/**
|
||||
* id of the native sbom report
|
||||
*/
|
||||
report_id?: string;
|
||||
|
||||
/**
|
||||
* The start time of the scan process that generating report
|
||||
*/
|
||||
start_time?: string;
|
||||
|
||||
/**
|
||||
* The end time of the scan process that generating report
|
||||
*/
|
||||
end_time?: string;
|
||||
|
||||
/**
|
||||
* The status of the generate SBOM process
|
||||
*/
|
||||
scan_status?: string;
|
||||
|
||||
/**
|
||||
* The digest of the generated SBOM accessory
|
||||
*/
|
||||
sbom_digest?: string;
|
||||
|
||||
/**
|
||||
* The seconds spent for generating the report
|
||||
*/
|
||||
duration?: number;
|
||||
scanner?: Scanner;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<div class="bar-wrapper">
|
||||
<div *ngIf="queued" class="bar-state">
|
||||
<span class="label label-orange">{{
|
||||
'SBOM.STATE.QUEUED' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div *ngIf="error" class="bar-state bar-state-error">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="error-text"
|
||||
target="_blank"
|
||||
[href]="viewLog()">
|
||||
<clr-icon shape="error" class="is-error" size="24"></clr-icon>
|
||||
{{ 'SBOM.STATE.ERROR' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="generating" class="bar-state bar-state-chart">
|
||||
<div>{{ 'SBOM.STATE.SCANNING' | translate }}</div>
|
||||
<div class="progress loop loop-height"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart">
|
||||
<hbr-sbom-tip-histogram
|
||||
[scanner]="getScanner()"
|
||||
[artifactDigest]="artifactDigest"
|
||||
[sbomSummary]="sbomOverview"
|
||||
[sbomDigest]="sbomDigest"></hbr-sbom-tip-histogram>
|
||||
</div>
|
||||
<div *ngIf="stopped" class="bar-state">
|
||||
<span class="label stopped">{{
|
||||
'SBOM.STATE.STOPPED' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div *ngIf="otherStatus" class="bar-state">
|
||||
<span class="label not-scan">{{
|
||||
'SBOM.STATE.OTHER_STATUS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,245 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ResultSbomComponent } from './sbom-scan.component';
|
||||
import {
|
||||
ScanningResultDefaultService,
|
||||
ScanningResultService,
|
||||
} from '../../../../../shared/services';
|
||||
import { SBOM_SCAN_STATUS } from '../../../../../shared/units/utils';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { SbomTipHistogramComponent } from './sbom-tip-histogram/sbom-tip-histogram.component';
|
||||
import { SBOMOverview } from './sbom-overview';
|
||||
import { of, timer } from 'rxjs';
|
||||
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
||||
import { Artifact } from 'ng-swagger-gen/models';
|
||||
|
||||
describe('ResultSbomComponent (inline template)', () => {
|
||||
let component: ResultSbomComponent;
|
||||
let fixture: ComponentFixture<ResultSbomComponent>;
|
||||
let mockData: SBOMOverview = {
|
||||
scan_status: SBOM_SCAN_STATUS.SUCCESS,
|
||||
end_time: new Date().toUTCString(),
|
||||
};
|
||||
const mockedSbomDigest =
|
||||
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
||||
const mockedSbomOverview = {
|
||||
report_id: '12345',
|
||||
scan_status: 'Error',
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
};
|
||||
const mockedCloneSbomOverview = {
|
||||
report_id: '12346',
|
||||
scan_status: 'Pending',
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
};
|
||||
const FakedScanService = {
|
||||
scanArtifact: () => of({}),
|
||||
stopScanArtifact: () => of({}),
|
||||
};
|
||||
const FakedArtifactService = {
|
||||
getArtifact: () =>
|
||||
of({
|
||||
accessories: null,
|
||||
addition_links: {
|
||||
build_history: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/build_history',
|
||||
},
|
||||
vulnerabilities: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/vulnerabilities',
|
||||
},
|
||||
},
|
||||
digest: 'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3',
|
||||
extra_attrs: {
|
||||
architecture: 'amd64',
|
||||
author: '',
|
||||
config: {
|
||||
Env: [
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
],
|
||||
WorkingDir: '/',
|
||||
},
|
||||
created: '2024-01-10T10:05:33.2702206Z',
|
||||
os: 'linux',
|
||||
},
|
||||
icon: 'sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06',
|
||||
id: 3,
|
||||
labels: null,
|
||||
manifest_media_type:
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
media_type: 'application/vnd.docker.container.image.v1+json',
|
||||
project_id: 3,
|
||||
pull_time: '2024-04-02T01:50:58.332Z',
|
||||
push_time: '2024-03-06T09:47:08.163Z',
|
||||
references: null,
|
||||
repository_id: 2,
|
||||
sbom_overview: {
|
||||
duration: 2,
|
||||
end_time: '2024-04-02T01:50:59.406Z',
|
||||
sbom_digest:
|
||||
'sha256:8cca43ea666e0e7990c2433e3b185313e6ba303cc7a3124bb767823c79fb74a6',
|
||||
scan_status: 'Success',
|
||||
start_time: '2024-04-02T01:50:57.176Z',
|
||||
},
|
||||
size: 3957,
|
||||
tags: null,
|
||||
type: 'IMAGE',
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [ResultSbomComponent, SbomTipHistogramComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ScanningResultService,
|
||||
useValue: ScanningResultDefaultService,
|
||||
},
|
||||
{
|
||||
provide: ScanService,
|
||||
useValue: FakedScanService,
|
||||
},
|
||||
{
|
||||
provide: ArtifactService,
|
||||
useValue: FakedArtifactService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResultSbomComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.repoName = 'mockRepo';
|
||||
component.artifactDigest = mockedSbomDigest;
|
||||
component.sbomDigest = mockedSbomDigest;
|
||||
component.sbomOverview = mockData;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should show "scan stopped" if status is STOPPED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.STOPPED;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('span');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('SBOM.STATE.STOPPED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show progress if status is SCANNING', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.RUNNING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.progress');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show QUEUED if status is QUEUED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.PENDING;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.bar-state');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('span');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent).toEqual('SBOM.STATE.QUEUED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show summary bar chart if status is COMPLETED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.SUCCESS;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('a');
|
||||
expect(el).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent getScanner', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.getScanner()).toBeUndefined();
|
||||
component.sbomOverview = mockedSbomOverview;
|
||||
expect(component.getScanner()).toBe(mockedSbomOverview.scanner);
|
||||
component.projectName = 'test';
|
||||
component.repoName = 'ui';
|
||||
component.artifactDigest = 'dg';
|
||||
expect(component.viewLog()).toBe(
|
||||
'/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log'
|
||||
);
|
||||
component.copyValue(mockedCloneSbomOverview);
|
||||
expect(component.sbomOverview.report_id).toBe(
|
||||
mockedCloneSbomOverview.report_id
|
||||
);
|
||||
});
|
||||
it('Test ResultSbomComponent status', () => {
|
||||
component.sbomOverview = mockedSbomOverview;
|
||||
fixture.detectChanges();
|
||||
expect(component.status).toBe(SBOM_SCAN_STATUS.ERROR);
|
||||
expect(component.completed).toBeFalsy();
|
||||
expect(component.queued).toBeFalsy();
|
||||
expect(component.generating).toBeFalsy();
|
||||
expect(component.stopped).toBeFalsy();
|
||||
expect(component.otherStatus).toBeFalsy();
|
||||
expect(component.error).toBeTruthy();
|
||||
});
|
||||
it('Test ResultSbomComponent ngOnDestroy', () => {
|
||||
component.stateCheckTimer = timer(0, 10000).subscribe(() => {});
|
||||
component.ngOnDestroy();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
expect(component.generateSbomSubscription).toBeNull();
|
||||
expect(component.stopSubscription).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent generateSbom', () => {
|
||||
fixture.detectChanges();
|
||||
component.generateSbom();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onSubmitting).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent stopSbom', () => {
|
||||
fixture.detectChanges();
|
||||
component.stopSbom();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onStopping).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent getSbomOverview', () => {
|
||||
fixture.detectChanges();
|
||||
component.getSbomOverview();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.stateCheckTimer).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,327 @@
|
|||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { Subscription, timer } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { ScannerVo } from '../../../../../shared/services';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import {
|
||||
clone,
|
||||
CURRENT_BASE_HREF,
|
||||
dbEncodeURIComponent,
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
SBOM_SCAN_STATUS,
|
||||
} from '../../../../../shared/units/utils';
|
||||
import { ArtifactService } from '../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact';
|
||||
import {
|
||||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../../../services/event-service/event.service';
|
||||
import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service';
|
||||
import { ScanType } from 'ng-swagger-gen/models';
|
||||
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
||||
import { SBOMOverview } from './sbom-overview';
|
||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||
const RETRY_TIMES: number = 3;
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-sbom-bar',
|
||||
templateUrl: './sbom-scan-component.html',
|
||||
styleUrls: ['./scanning.scss'],
|
||||
})
|
||||
export class ResultSbomComponent implements OnInit, OnDestroy {
|
||||
@Input() inputScanner: ScannerVo;
|
||||
@Input() repoName: string = '';
|
||||
@Input() projectName: string = '';
|
||||
@Input() projectId: string = '';
|
||||
@Input() artifactDigest: string = '';
|
||||
@Input() sbomDigest: string = '';
|
||||
@Input() sbomOverview: SBOMOverview;
|
||||
onSubmitting: boolean = false;
|
||||
onStopping: boolean = false;
|
||||
retryCounter: number = 0;
|
||||
stateCheckTimer: Subscription;
|
||||
generateSbomSubscription: Subscription;
|
||||
stopSubscription: Subscription;
|
||||
timerHandler: any;
|
||||
@Output()
|
||||
submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
// if sending stop scan request is finished, emit to farther component
|
||||
@Output()
|
||||
submitStopFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output()
|
||||
scanFinished: EventEmitter<Artifact> = new EventEmitter<Artifact>();
|
||||
|
||||
constructor(
|
||||
private artifactService: ArtifactService,
|
||||
private scanService: ScanService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private eventService: EventService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (
|
||||
(this.status === SBOM_SCAN_STATUS.RUNNING ||
|
||||
this.status === SBOM_SCAN_STATUS.PENDING) &&
|
||||
!this.stateCheckTimer
|
||||
) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(0, STATE_CHECK_INTERVAL).subscribe(
|
||||
() => {
|
||||
this.getSbomOverview();
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!this.generateSbomSubscription) {
|
||||
this.generateSbomSubscription = this.eventService.subscribe(
|
||||
HarborEvent.START_GENERATE_SBOM,
|
||||
(artifactDigest: string) => {
|
||||
let myFullTag: string =
|
||||
this.repoName + '/' + this.artifactDigest;
|
||||
if (myFullTag === artifactDigest) {
|
||||
this.generateSbom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!this.stopSubscription) {
|
||||
this.stopSubscription = this.eventService.subscribe(
|
||||
HarborEvent.STOP_SBOM_ARTIFACT,
|
||||
(artifactDigest: string) => {
|
||||
let myFullTag: string =
|
||||
this.repoName + '/' + this.artifactDigest;
|
||||
if (myFullTag === artifactDigest) {
|
||||
this.stopSbom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
if (this.generateSbomSubscription) {
|
||||
this.generateSbomSubscription.unsubscribe();
|
||||
this.generateSbomSubscription = null;
|
||||
}
|
||||
if (this.stopSubscription) {
|
||||
this.stopSubscription.unsubscribe();
|
||||
this.stopSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get vulnerability scanning status
|
||||
public get status(): string {
|
||||
if (this.sbomOverview && this.sbomOverview.scan_status) {
|
||||
return this.sbomOverview.scan_status;
|
||||
}
|
||||
return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM;
|
||||
}
|
||||
|
||||
public get completed(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.SUCCESS;
|
||||
}
|
||||
|
||||
public get error(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.ERROR;
|
||||
}
|
||||
|
||||
public get queued(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.PENDING;
|
||||
}
|
||||
|
||||
public get generating(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.RUNNING;
|
||||
}
|
||||
|
||||
public get stopped(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.STOPPED;
|
||||
}
|
||||
|
||||
public get otherStatus(): boolean {
|
||||
return !(
|
||||
this.completed ||
|
||||
this.error ||
|
||||
this.queued ||
|
||||
this.generating ||
|
||||
this.stopped
|
||||
);
|
||||
}
|
||||
|
||||
generateSbom(): void {
|
||||
if (this.onSubmitting) {
|
||||
// Avoid duplicated submitting
|
||||
console.error('duplicated submit');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
console.error('bad repository or tag');
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmitting = true;
|
||||
|
||||
this.scanService
|
||||
.scanArtifact({
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
// scanType: <ScanType>{
|
||||
// scan_type: ScanTypes.SBOM,
|
||||
// },
|
||||
})
|
||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||
.subscribe(
|
||||
() => {
|
||||
this.onSubmitting = false;
|
||||
// Forcely change status to queued after successful submitting
|
||||
this.sbomOverview = {
|
||||
scan_status: SBOM_SCAN_STATUS.PENDING,
|
||||
};
|
||||
// Start check status util the job is done
|
||||
if (!this.stateCheckTimer) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(
|
||||
STATE_CHECK_INTERVAL,
|
||||
STATE_CHECK_INTERVAL
|
||||
).subscribe(() => {
|
||||
this.getSbomOverview();
|
||||
});
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.onSubmitting = false;
|
||||
if (error && error.error && error.error.code === 409) {
|
||||
console.error(error.error.message);
|
||||
} else {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSbomOverview(): void {
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
return;
|
||||
}
|
||||
this.artifactService
|
||||
.getArtifact({
|
||||
projectName: this.projectName,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: this.artifactDigest,
|
||||
// withSbomOverview: true,
|
||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
})
|
||||
.subscribe(
|
||||
(artifact: Artifact) => {
|
||||
// To keep the same summary reference, use value copy.
|
||||
// if (artifact.sbom_overview) {
|
||||
// this.copyValue(artifact.sbom_overview);
|
||||
// }
|
||||
if (!this.queued && !this.generating) {
|
||||
// Scanning should be done
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
this.scanFinished.emit(artifact);
|
||||
}
|
||||
this.eventService.publish(
|
||||
HarborEvent.UPDATE_SBOM_INFO,
|
||||
artifact
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
this.retryCounter++;
|
||||
if (this.retryCounter >= RETRY_TIMES) {
|
||||
// Stop timer
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
this.retryCounter = 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
copyValue(newVal: SBOMOverview): void {
|
||||
if (!this.sbomOverview || !newVal || !newVal.scan_status) {
|
||||
return;
|
||||
}
|
||||
this.sbomOverview = clone(newVal);
|
||||
}
|
||||
|
||||
viewLog(): string {
|
||||
return `${CURRENT_BASE_HREF}/projects/${
|
||||
this.projectName
|
||||
}/repositories/${dbEncodeURIComponent(this.repoName)}/artifacts/${
|
||||
this.artifactDigest
|
||||
}/scan/${this.sbomOverview.report_id}/log`;
|
||||
}
|
||||
|
||||
getScanner(): ScannerVo {
|
||||
if (this.sbomOverview && this.sbomOverview.scanner) {
|
||||
return this.sbomOverview.scanner;
|
||||
}
|
||||
return this.inputScanner;
|
||||
}
|
||||
|
||||
stopSbom() {
|
||||
if (this.onStopping) {
|
||||
// Avoid duplicated stopping command
|
||||
console.error('duplicated stopping command for SBOM generation');
|
||||
return;
|
||||
}
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
console.error('bad repository or artifact');
|
||||
return;
|
||||
}
|
||||
this.onStopping = true;
|
||||
|
||||
this.scanService
|
||||
.stopScanArtifact({
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
scanType: <ScanType>{
|
||||
scan_type: ScanTypes.SBOM,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.submitStopFinish.emit(false);
|
||||
this.onStopping = false;
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
() => {
|
||||
// Start check status util the job is done
|
||||
if (!this.stateCheckTimer) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(
|
||||
STATE_CHECK_INTERVAL,
|
||||
STATE_CHECK_INTERVAL
|
||||
).subscribe(() => {
|
||||
this.getSbomOverview();
|
||||
});
|
||||
}
|
||||
this.errorHandler.info('SBOM.TRIGGER_STOP_SUCCESS');
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<div class="tip-wrapper width-215">
|
||||
<clr-tooltip>
|
||||
<div clrTooltipTrigger class="tip-block">
|
||||
<div *ngIf="!noSbom" class="circle-block">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="digest margin-left-5"
|
||||
(click)="goIntoArtifactSbomSummaryPage()"
|
||||
title="{{ 'SBOM.Details' | translate }">
|
||||
{{ 'SBOM.Details' | translate }}</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="noSbom"
|
||||
class="pl-1 margin-left-5 tip-wrapper bar-block-none shadow-none width-150">
|
||||
{{ 'SBOM.NO_SBOM' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<clr-tooltip-content
|
||||
class="w-800"
|
||||
[clrPosition]="'right'"
|
||||
[clrSize]="'lg'"
|
||||
*clrIfOpen>
|
||||
<div class="bar-tooltip-font-larger">
|
||||
<div
|
||||
*ngIf="!noSbom"
|
||||
class="level-border clr-display-inline-block margin-right-5">
|
||||
<div>
|
||||
{{ 'SBOM.PACKAGES' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="noSbom">
|
||||
<span>{{ 'SBOM.NO_SBOM' | translate }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<hr />
|
||||
<div *ngIf="scanner">
|
||||
<span class="bar-scanning-time">{{
|
||||
'SCANNER.SCANNED_BY' | translate
|
||||
}}</span>
|
||||
<span class="margin-left-5">{{ getScannerInfo() }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{
|
||||
'SCANNER.DURATION' | translate
|
||||
}}</span>
|
||||
<span class="margin-left-5">{{ duration() }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time"
|
||||
>{{ 'SBOM.CHART.SCANNING_TIME' | translate }}
|
||||
</span>
|
||||
<span>{{ completeTimestamp | harborDatetime : 'short' }}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
<clr-tooltip class="margin-left-5" *ngIf="isLimitedSuccess()">
|
||||
<div clrTooltipTrigger>
|
||||
<clr-icon
|
||||
shape="exclamation-triangle"
|
||||
size="20"
|
||||
class="is-warning"></clr-icon>
|
||||
</div>
|
||||
<clr-tooltip-content [clrSize]="'lg'" *clrIfOpen>
|
||||
<div class="font-weight-600">
|
||||
<span class="bar-scanning-time"
|
||||
>{{ 'SBOM.CHART.SCANNING_PERCENT' | translate }}
|
||||
</span>
|
||||
<span>{{ completePercent }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{
|
||||
'SBOM.CHART.SCANNING_PERCENT_EXPLAIN' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</div>
|
|
@ -0,0 +1,60 @@
|
|||
.tip-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-tooltip-font-larger {
|
||||
span {
|
||||
font-size: 16px;
|
||||
vertical-align: middle
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 0;
|
||||
border-color: #aaa;
|
||||
margin: 6px -10px;
|
||||
}
|
||||
|
||||
.font-weight-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.margin-left-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.width-215 {
|
||||
width: 215px;
|
||||
}
|
||||
|
||||
.width-150 {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.level-border>div{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
border-radius: 50%;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.tip-block {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.margin-right-5 {
|
||||
margin-right: 5px;
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SbomTipHistogramComponent } from './sbom-tip-histogram.component';
|
||||
import { of } from 'rxjs';
|
||||
import { Project } from '../../../../../../../app/base/project/project';
|
||||
import { Artifact } from 'ng-swagger-gen/models';
|
||||
|
||||
describe('SbomTipHistogramComponent', () => {
|
||||
let component: SbomTipHistogramComponent;
|
||||
let fixture: ComponentFixture<SbomTipHistogramComponent>;
|
||||
const mockRouter = {
|
||||
navigate: () => {},
|
||||
};
|
||||
const mockedArtifact: Artifact = {
|
||||
id: 123,
|
||||
type: 'IMAGE',
|
||||
};
|
||||
const mockedScanner = {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
};
|
||||
const mockActivatedRoute = {
|
||||
RouterparamMap: of({ get: key => 'value' }),
|
||||
snapshot: {
|
||||
params: {
|
||||
repo: 'test',
|
||||
digest: 'ABC',
|
||||
subscribe: () => {
|
||||
return of(null);
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
params: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
artifactResolver: [mockedArtifact, new Project()],
|
||||
},
|
||||
},
|
||||
data: of({
|
||||
projectResolver: {
|
||||
ismember: true,
|
||||
role_name: 'maintainer',
|
||||
},
|
||||
}),
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
TranslateService,
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
],
|
||||
declarations: [SbomTipHistogramComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SbomTipHistogramComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent basic functions', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.isLimitedSuccess()).toBeFalsy();
|
||||
expect(component.noSbom).toBeTruthy();
|
||||
expect(component.isThemeLight()).toBeFalsy();
|
||||
expect(component.duration()).toBe('0');
|
||||
expect(component.completePercent).toBe('0%');
|
||||
});
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent completeTimestamp', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
component.sbomSummary.end_time = new Date('2024-04-08 00:01:02');
|
||||
expect(component.completeTimestamp).toBe(
|
||||
component.sbomSummary.end_time
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent getScannerInfo', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.getScannerInfo()).toBe('');
|
||||
component.scanner = mockedScanner;
|
||||
expect(component.getScannerInfo()).toBe(
|
||||
`${mockedScanner.name}@${mockedScanner.version}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ScannerVo, SbomSummary } from '../../../../../../shared/services';
|
||||
import { SBOM_SCAN_STATUS } from '../../../../../../shared/units/utils';
|
||||
import {
|
||||
UN_LOGGED_PARAM,
|
||||
YES,
|
||||
} from '../../../../../../account/sign-in/sign-in.service';
|
||||
import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme';
|
||||
import { ScanTypes } from '../../../../../../shared/entities/shared.const';
|
||||
|
||||
const MIN = 60;
|
||||
const MIN_STR = 'min ';
|
||||
const SEC_STR = 'sec';
|
||||
const SUCCESS_PCT: number = 100;
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-sbom-tip-histogram',
|
||||
templateUrl: './sbom-tip-histogram.component.html',
|
||||
styleUrls: ['./sbom-tip-histogram.component.scss'],
|
||||
})
|
||||
export class SbomTipHistogramComponent {
|
||||
@Input() scanner: ScannerVo;
|
||||
@Input() sbomSummary: SbomSummary = {
|
||||
scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM,
|
||||
};
|
||||
@Input() artifactDigest: string = '';
|
||||
@Input() sbomDigest: string = '';
|
||||
constructor(
|
||||
private translate: TranslateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
duration(): string {
|
||||
if (this.sbomSummary && this.sbomSummary.duration) {
|
||||
let str = '';
|
||||
const min = Math.floor(this.sbomSummary.duration / MIN);
|
||||
if (min) {
|
||||
str += min + ' ' + MIN_STR;
|
||||
}
|
||||
const sec = this.sbomSummary.duration % MIN;
|
||||
if (sec) {
|
||||
str += sec + ' ' + SEC_STR;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
public get completePercent(): string {
|
||||
return this.sbomSummary.scan_status === SBOM_SCAN_STATUS.SUCCESS
|
||||
? `100%`
|
||||
: '0%';
|
||||
}
|
||||
isLimitedSuccess(): boolean {
|
||||
return (
|
||||
this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT
|
||||
);
|
||||
}
|
||||
get completeTimestamp(): Date {
|
||||
return this.sbomSummary && this.sbomSummary.end_time
|
||||
? this.sbomSummary.end_time
|
||||
: new Date();
|
||||
}
|
||||
|
||||
get noSbom(): boolean {
|
||||
return (
|
||||
this.sbomSummary.scan_status === SBOM_SCAN_STATUS.NOT_GENERATED_SBOM
|
||||
);
|
||||
}
|
||||
|
||||
isThemeLight() {
|
||||
return localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT;
|
||||
}
|
||||
|
||||
getScannerInfo(): string {
|
||||
if (this.scanner) {
|
||||
if (this.scanner.name && this.scanner.version) {
|
||||
return `${this.scanner.name}@${this.scanner.version}`;
|
||||
}
|
||||
if (this.scanner.name && !this.scanner.version) {
|
||||
return `${this.scanner.name}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
goIntoArtifactSbomSummaryPage(): void {
|
||||
const relativeRouterLink: string[] = ['artifacts', this.artifactDigest];
|
||||
if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) {
|
||||
this.router.navigate(relativeRouterLink, {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParams: {
|
||||
[UN_LOGGED_PARAM]: YES,
|
||||
sbomDigest: this.sbomDigest ?? '',
|
||||
tab: ScanTypes.SBOM,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(relativeRouterLink, {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParams: {
|
||||
sbomDigest: this.sbomDigest ?? '',
|
||||
tab: ScanTypes.SBOM,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
.bar-wrapper {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.bar-state {
|
||||
.unknow-text {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-chart {
|
||||
.loop-height {
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-error {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scanning-button {
|
||||
height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
top: -12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
|
||||
.bar-tooltip-font-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar-summary {
|
||||
margin-top: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-summary-item {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.bar-summary-item span:nth-child(1){
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bar-summary-item span:nth-child(2){
|
||||
width: 28px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.tip-icon-medium {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.tip-icon-low {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.font-color-green{
|
||||
color:green;
|
||||
}
|
||||
/* stylelint-disable */
|
||||
.bar-tooltip-font-larger span{
|
||||
font-size:16px;
|
||||
vertical-align:middle
|
||||
}
|
||||
|
||||
hr{
|
||||
border-bottom: 0;
|
||||
border-color: #aaa;
|
||||
margin: 6px -10px;
|
||||
}
|
||||
|
||||
.font-weight-600{
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.rightPos{
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 35px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.mt-3px {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.label-critical {
|
||||
background:#ff4d2e;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background:#ff8f3d!important;
|
||||
color:#000!important;
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
background-color: #ffce66;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-low {
|
||||
background: #fff1ad;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-none {
|
||||
background-color: #2ec0ff;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
hbr-vulnerability-bar {
|
||||
.label,.not-scan {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.stopped {
|
||||
border-color: #cccc15;
|
||||
}
|
|
@ -11,14 +11,43 @@ import {
|
|||
import { VULNERABILITY_SCAN_STATUS } from '../../../../../shared/units/utils';
|
||||
import { NativeReportSummary } from '../../../../../../../ng-swagger-gen/models/native-report-summary';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { of, timer } from 'rxjs';
|
||||
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
||||
|
||||
describe('ResultBarChartComponent (inline template)', () => {
|
||||
let component: ResultBarChartComponent;
|
||||
let fixture: ComponentFixture<ResultBarChartComponent>;
|
||||
const mockedSbomDigest =
|
||||
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
||||
let mockData: NativeReportSummary = {
|
||||
report_id: '12345',
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
|
||||
severity: 'High',
|
||||
end_time: new Date().toUTCString(),
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
summary: {
|
||||
High: 5,
|
||||
Low: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
let mockCloneData: NativeReportSummary = {
|
||||
report_id: '123456',
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
|
||||
severity: 'High',
|
||||
end_time: new Date().toUTCString(),
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.3',
|
||||
},
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
|
@ -29,6 +58,59 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const FakedScanService = {
|
||||
scanArtifact: () => of({}),
|
||||
stopScanArtifact: () => of({}),
|
||||
};
|
||||
const FakedArtifactService = {
|
||||
getArtifact: () =>
|
||||
of({
|
||||
accessories: null,
|
||||
addition_links: {
|
||||
build_history: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/build_history',
|
||||
},
|
||||
vulnerabilities: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/vulnerabilities',
|
||||
},
|
||||
},
|
||||
digest: 'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3',
|
||||
extra_attrs: {
|
||||
architecture: 'amd64',
|
||||
author: '',
|
||||
config: {
|
||||
Env: [
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
],
|
||||
WorkingDir: '/',
|
||||
},
|
||||
created: '2024-01-10T10:05:33.2702206Z',
|
||||
os: 'linux',
|
||||
},
|
||||
icon: 'sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06',
|
||||
id: 3,
|
||||
labels: null,
|
||||
manifest_media_type:
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
media_type: 'application/vnd.docker.container.image.v1+json',
|
||||
project_id: 3,
|
||||
pull_time: '2024-04-02T01:50:58.332Z',
|
||||
push_time: '2024-03-06T09:47:08.163Z',
|
||||
references: null,
|
||||
repository_id: 2,
|
||||
scan_overview: {
|
||||
duration: 2,
|
||||
end_time: '2024-04-02T01:50:59.406Z',
|
||||
scan_status: 'Success',
|
||||
start_time: '2024-04-02T01:50:57.176Z',
|
||||
},
|
||||
size: 3957,
|
||||
tags: null,
|
||||
type: 'IMAGE',
|
||||
}),
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
|
@ -43,6 +125,14 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||
useValue: ScanningResultDefaultService,
|
||||
},
|
||||
{ provide: JobLogService, useValue: JobLogDefaultService },
|
||||
{
|
||||
provide: ScanService,
|
||||
useValue: FakedScanService,
|
||||
},
|
||||
{
|
||||
provide: ArtifactService,
|
||||
useValue: FakedArtifactService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
@ -52,6 +142,8 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||
component = fixture.componentInstance;
|
||||
component.artifactDigest = 'mockTag';
|
||||
component.summary = mockData;
|
||||
component.repoName = 'mockRepo';
|
||||
component.artifactDigest = mockedSbomDigest;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@ -109,4 +201,69 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||
expect(el).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent getScanner', () => {
|
||||
fixture.detectChanges();
|
||||
component.summary = mockData;
|
||||
expect(component.getScanner()).toBe(mockData.scanner);
|
||||
component.projectName = 'test';
|
||||
component.repoName = 'ui';
|
||||
component.artifactDigest = 'dg';
|
||||
expect(component.viewLog()).toBe(
|
||||
'/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log'
|
||||
);
|
||||
component.copyValue(mockCloneData);
|
||||
expect(component.summary.report_id).toBe(mockCloneData.report_id);
|
||||
});
|
||||
it('Test ResultBarChartComponent status', () => {
|
||||
fixture.detectChanges();
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
expect(component.status).toBe(VULNERABILITY_SCAN_STATUS.SUCCESS);
|
||||
expect(component.completed).toBeTruthy();
|
||||
expect(component.queued).toBeFalsy();
|
||||
expect(component.scanning).toBeFalsy();
|
||||
expect(component.stopped).toBeFalsy();
|
||||
expect(component.otherStatus).toBeFalsy();
|
||||
expect(component.error).toBeFalsy();
|
||||
});
|
||||
it('Test ResultBarChartComponent ngOnDestroy', () => {
|
||||
component.stateCheckTimer = timer(0, 10000).subscribe(() => {});
|
||||
component.ngOnDestroy();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
expect(component.scanSubscription).toBeNull();
|
||||
expect(component.stopSubscription).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent scanNow', () => {
|
||||
fixture.detectChanges();
|
||||
component.scanNow();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onSubmitting).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent stopScan', () => {
|
||||
fixture.detectChanges();
|
||||
component.stopScan();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onStopping).toBeFalse();
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent getSummary', () => {
|
||||
fixture.detectChanges();
|
||||
// component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
component.getSummary();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.summary.scan_status).toBe(
|
||||
VULNERABILITY_SCAN_STATUS.SUCCESS
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,8 @@ import {
|
|||
HarborEvent,
|
||||
} from '../../../../../services/event-service/event.service';
|
||||
import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service';
|
||||
import { ScanType } from 'ng-swagger-gen/models';
|
||||
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
||||
|
||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||
const RETRY_TIMES: number = 3;
|
||||
|
@ -110,7 +112,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||
this.scanSubscription.unsubscribe();
|
||||
this.scanSubscription = null;
|
||||
}
|
||||
if (!this.stopSubscription) {
|
||||
if (this.stopSubscription) {
|
||||
this.stopSubscription.unsubscribe();
|
||||
this.stopSubscription = null;
|
||||
}
|
||||
|
@ -171,6 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
// scanType: <ScanType>{
|
||||
// scan_type: ScanTypes.VULNERABILITY,
|
||||
// },
|
||||
})
|
||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||
.subscribe(
|
||||
|
@ -286,6 +291,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
scanType: <ScanType>{
|
||||
scan_type: ScanTypes.VULNERABILITY,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
width: 90%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,8 +76,11 @@ export enum HarborEvent {
|
|||
SCROLL_TO_POSITION = 'scrollToPosition',
|
||||
REFRESH_PROJECT_INFO = 'refreshProjectInfo',
|
||||
START_SCAN_ARTIFACT = 'startScanArtifact',
|
||||
START_GENERATE_SBOM = 'startGenerateSbom',
|
||||
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
|
||||
STOP_SBOM_ARTIFACT = 'stopSbomArtifact',
|
||||
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
|
||||
UPDATE_SBOM_INFO = 'UpdateSbomInfo',
|
||||
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
|
||||
DELETE_ACCESSORY = 'deleteAccessory',
|
||||
COPY_DIGEST = 'copyDigest',
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
ActivatedRouteSnapshot,
|
||||
} from '@angular/router';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { map, catchError, mergeMap } from 'rxjs/operators';
|
||||
import { catchError, mergeMap } from 'rxjs/operators';
|
||||
import { Artifact } from '../../../../ng-swagger-gen/models/artifact';
|
||||
import { ArtifactService } from '../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { Project } from '../../base/project/project';
|
||||
|
@ -51,6 +51,7 @@ export class ArtifactDetailRoutingResolverService {
|
|||
projectName: project.name,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
withImmutableStatus: true,
|
||||
}),
|
||||
|
|
|
@ -382,3 +382,8 @@ export const stringsForClarity: Partial<ClrCommonStrings> = {
|
|||
datepickerSelectYearText: 'CLARITY.DATE_PICKER_SELECT_YEAR_TEXT',
|
||||
datepickerSelectedLabel: 'CLARITY.DATE_PICKER_SELECTED_LABEL',
|
||||
};
|
||||
|
||||
export enum ScanTypes {
|
||||
SBOM = 'sbom',
|
||||
VULNERABILITY = 'vulnerability',
|
||||
}
|
||||
|
|
|
@ -211,6 +211,16 @@ export interface VulnerabilitySummary {
|
|||
scanner?: ScannerVo;
|
||||
complete_percent?: number;
|
||||
}
|
||||
export interface SbomSummary {
|
||||
report_id?: string;
|
||||
sbom_digest?: string;
|
||||
scan_status?: string;
|
||||
duration?: number;
|
||||
start_time?: Date;
|
||||
end_time?: Date;
|
||||
scanner?: ScannerVo;
|
||||
complete_percent?: number;
|
||||
}
|
||||
export interface ScannerVo {
|
||||
name?: string;
|
||||
vendor?: string;
|
||||
|
|
|
@ -105,6 +105,13 @@ export const USERSTATICPERMISSION = {
|
|||
READ: 'read',
|
||||
},
|
||||
},
|
||||
REPOSITORY_TAG_SBOM_JOB: {
|
||||
KEY: 'sbom',
|
||||
VALUE: {
|
||||
CREATE: 'create',
|
||||
READ: 'read',
|
||||
},
|
||||
},
|
||||
REPOSITORY_ARTIFACT_LABEL: {
|
||||
KEY: 'artifact-label',
|
||||
VALUE: {
|
||||
|
|
|
@ -275,6 +275,21 @@ export const VULNERABILITY_SCAN_STATUS = {
|
|||
SUCCESS: 'Success',
|
||||
SCHEDULED: 'Scheduled',
|
||||
};
|
||||
|
||||
/**
|
||||
* The state of sbom generation
|
||||
*/
|
||||
export const SBOM_SCAN_STATUS = {
|
||||
// front-end status
|
||||
NOT_GENERATED_SBOM: 'Not generated SBOM',
|
||||
// back-end status
|
||||
PENDING: 'Pending',
|
||||
RUNNING: 'Running',
|
||||
ERROR: 'Error',
|
||||
STOPPED: 'Stopped',
|
||||
SUCCESS: 'Success',
|
||||
SCHEDULED: 'Scheduled',
|
||||
};
|
||||
/**
|
||||
* The severity of vulnerability scanning
|
||||
*/
|
||||
|
|
|
@ -777,6 +777,7 @@
|
|||
"ARTIFACTS": "Artefakte",
|
||||
"SIZE": "Größe",
|
||||
"VULNERABILITY": "Schwachstellen",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Build History",
|
||||
"SIGNED": "Signiert",
|
||||
"AUTHOR": "Autor",
|
||||
|
@ -1027,6 +1028,41 @@
|
|||
"IN_PROGRESS": "Suche...",
|
||||
"BACK": "Zurück"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Nicht gescannt",
|
||||
|
@ -1107,6 +1143,7 @@
|
|||
"ALL": "Alle",
|
||||
"PLACEHOLDER": "Keine Artefakte gefunden!",
|
||||
"SCAN_UNSUPPORTED": "Nicht unterstützt",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Zusammenfassung",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
|
|
@ -778,6 +778,7 @@
|
|||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Build History",
|
||||
"SIGNED": "Signed",
|
||||
"AUTHOR": "Author",
|
||||
|
@ -1028,6 +1029,41 @@
|
|||
"IN_PROGRESS": "Search...",
|
||||
"BACK": "Back"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
|
@ -1108,6 +1144,7 @@
|
|||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Summary",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
|
|
@ -778,6 +778,7 @@
|
|||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Construir Historia",
|
||||
"SIGNED": "Firmada",
|
||||
"AUTHOR": "Autor",
|
||||
|
@ -1026,6 +1027,41 @@
|
|||
"IN_PROGRESS": "Buscar...",
|
||||
"BACK": "Volver"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
|
@ -1106,6 +1142,7 @@
|
|||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Summary",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"VIC": "vSphere Integrated Containers",
|
||||
"MGMT": "Management",
|
||||
"REG": "Registre",
|
||||
"HARBOR_SWAGGER": "Harbor Swagger",
|
||||
"HARBOR_SWAGGER": "Swagger Harbor",
|
||||
"THEME_DARK_TEXT": "SOMBRE",
|
||||
"THEME_LIGHT_TEXT": "CLAIR"
|
||||
},
|
||||
|
@ -28,7 +28,7 @@
|
|||
"DELETE": "Supprimer",
|
||||
"LOG_IN": "S'identifier",
|
||||
"LOG_IN_OIDC": "Connexion via fournisseur OIDC",
|
||||
"LOG_IN_OIDC_WITH_PROVIDER_NAME": "LOGIN WITH {{providerName}}",
|
||||
"LOG_IN_OIDC_WITH_PROVIDER_NAME": "S'IDENTIFIER AVEC {{providerName}}",
|
||||
"SIGN_UP_LINK": "Ouvrir un compte",
|
||||
"SIGN_UP": "S'inscrire",
|
||||
"CONFIRM": "Confirmer",
|
||||
|
@ -130,7 +130,7 @@
|
|||
"ADMIN_RENAME_TIP": "Cliquez sur le bouton pour changer le nom d'utilisateur en \"admin@harbor.local\". Cette opération ne peut pas être annulée.",
|
||||
"RENAME_SUCCESS": "Renommage effectué !",
|
||||
"RENAME_CONFIRM_INFO": "Attention, changer le nom d'utilisateur pour \"admin@harbor.local\" ne peut pas être annulé.",
|
||||
"CLI_PASSWORD": "CLI secret",
|
||||
"CLI_PASSWORD": "Secret CLI",
|
||||
"CLI_PASSWORD_TIP": "Le secret CLI peut être utilisé comme mot de passe pour le client Docker ou Helm. Lorsque le mode d'authentification est OIDC, nous recommandons fortement d'utiliser des comptes robots, car les secrets CLI dépendent de la validité du jeton ID et nécessitent que l'utilisateur se connecte régulièrement à l'interface utilisateur pour rafraîchir le jeton.",
|
||||
"COPY_SUCCESS": "Copie effectuée",
|
||||
"COPY_ERROR": "Copie échouée",
|
||||
|
@ -248,12 +248,12 @@
|
|||
"INLINE_HELP_PUBLIC": "Lorsqu'un projet est mis en public, n'importe qui a l'autorisation de lire les dépôts sous ce projet, et l'utilisateur n'a pas besoin d'exécuter \"docker login\" avant de prendre des images de ce projet.",
|
||||
"OF": "sur",
|
||||
"COUNT_QUOTA": "Quota de nombre",
|
||||
"STORAGE_QUOTA": "Project quota limits",
|
||||
"STORAGE_QUOTA": "Quota du projet",
|
||||
"COUNT_QUOTA_TIP": "Entrez un entier entre '1' et '100000000', ou '-1' pour un quota illimité",
|
||||
"STORAGE_QUOTA_TIP": "La limite haute du quota de stockage n'accepte que des valeurs entières, au maximum '1024TB'. Entrez '-1' pour un quota illimité",
|
||||
"QUOTA_UNLIMIT_TIP": "The maximum logical space that can be used by the project. Pour un quota illimité, entrez '-1'.",
|
||||
"QUOTA_UNLIMIT_TIP": "Espace maximum logique pouvant être utilisé par le projet. Pour un quota illimité, entrez '-1'.",
|
||||
"TYPE": "Type",
|
||||
"PROXY_CACHE": "Proxy Cache",
|
||||
"PROXY_CACHE": "Cache proxy",
|
||||
"PROXY_CACHE_TOOLTIP": "Activez cette option pour permettre à ce projet d'agir comme un cache de pull pour un espace de noms particulier dans un registre cible. Harbor ne peut agir en tant que proxy que pour les registres DockerHub et Harbor.",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"PROXY_CACHE_ENDPOINT": "Endpoint du Proxy Cache",
|
||||
|
@ -287,9 +287,9 @@
|
|||
"SCAN": "Analyse des vulnérabilités",
|
||||
"AUTOSCAN_TOGGLE": "Analyse automatique des images lors de l'envoi",
|
||||
"AUTOSCAN_POLICY": "Analyser automatiquement les images lorsqu'elles sont envoyées au projet du registre.",
|
||||
"SBOM": "SBOM generation",
|
||||
"AUTOSBOM_TOGGLE": "Automatically generate SBOM on push",
|
||||
"AUTOSBOM_POLICY": "Automatically generate SBOM when the images are pushed to the project registry."
|
||||
"SBOM": "Génération de SBOM",
|
||||
"AUTOSBOM_TOGGLE": "Générer automatiquement un SBOM au push",
|
||||
"AUTOSBOM_POLICY": "Générer automatiquement un SBOM lorsque les images sont poussées sur le registre."
|
||||
},
|
||||
"MEMBER": {
|
||||
"NEW_USER": "Ajouter un nouveau membre",
|
||||
|
@ -339,7 +339,7 @@
|
|||
"SWITCH_TITLE": "Confirmez le changement de membres projet",
|
||||
"SWITCH_SUMMARY": "Voulez-vous changer les membres projet {{param}}?",
|
||||
"SET_ROLE": "Définir Role",
|
||||
"REMOVE": "Remove",
|
||||
"REMOVE": "Retirer",
|
||||
"GROUP_NAME_REQUIRED": "Le nom du groupe est requis",
|
||||
"NON_EXISTENT_GROUP": "Ce groupe n'existe pas",
|
||||
"GROUP_ALREADY_ADDED": "Ce groupe a déjà été ajouté au projet"
|
||||
|
@ -383,46 +383,46 @@
|
|||
"NEVER_EXPIRED": "Ne jamais expirer",
|
||||
"NAME_PREFIX": "Préfixe du nom du compte robot",
|
||||
"NAME_PREFIX_REQUIRED": "Le préfixe du nom du compte robot est obligatoire",
|
||||
"UPDATE": "Update",
|
||||
"AUDIT_LOG": "Audit Log",
|
||||
"PREHEAT_INSTANCE": "Preheat Instance",
|
||||
"PROJECT": "Project",
|
||||
"REPLICATION_POLICY": "Replication Policy",
|
||||
"REPLICATION": "Replication",
|
||||
"REPLICATION_ADAPTER": "Replication Adapter",
|
||||
"REGISTRY": "Registry",
|
||||
"SCAN_ALL": "Scan All",
|
||||
"SYSTEM_VOLUMES": "System Volumes",
|
||||
"GARBAGE_COLLECTION": "Garbage Collection",
|
||||
"PURGE_AUDIT": "Purge Audit",
|
||||
"UPDATE": "Mettre à jour",
|
||||
"AUDIT_LOG": "Log d'audit",
|
||||
"PREHEAT_INSTANCE": "Préchauffer l'instance",
|
||||
"PROJECT": "Projet",
|
||||
"REPLICATION_POLICY": "Politique de réplication",
|
||||
"REPLICATION": "Réplication",
|
||||
"REPLICATION_ADAPTER": "Adaptateur de réplication",
|
||||
"REGISTRY": "Registre",
|
||||
"SCAN_ALL": "Scanner tout",
|
||||
"SYSTEM_VOLUMES": "Volumes système",
|
||||
"GARBAGE_COLLECTION": "Purge",
|
||||
"PURGE_AUDIT": "Purger l'audit",
|
||||
"JOBSERVICE_MONITOR": "Job Service Monitor",
|
||||
"TAG_RETENTION": "Tag Retention",
|
||||
"TAG_RETENTION": "Rétention des tags",
|
||||
"SCANNER": "Scanner",
|
||||
"LABEL": "Label",
|
||||
"EXPORT_CVE": "Export CVE",
|
||||
"SECURITY_HUB": "Security Hub",
|
||||
"CATALOG": "Catalog",
|
||||
"METADATA": "Project Metadata",
|
||||
"REPOSITORY": "Repository",
|
||||
"ARTIFACT": "Artifact",
|
||||
"EXPORT_CVE": "Exporter les CVE",
|
||||
"SECURITY_HUB": "Centre de sécurité",
|
||||
"CATALOG": "Catalogue",
|
||||
"METADATA": "Métadonnées du projet",
|
||||
"REPOSITORY": "Dépôt",
|
||||
"ARTIFACT": "Artefact",
|
||||
"SCAN": "Scan",
|
||||
"TAG": "Tag",
|
||||
"ACCESSORY": "Accessory",
|
||||
"ARTIFACT_ADDITION": "Artifact Addition",
|
||||
"ARTIFACT_LABEL": "Artifact Label",
|
||||
"PREHEAT_POLICY": "Preheat Policy",
|
||||
"IMMUTABLE_TAG": "Immutable Tag",
|
||||
"ACCESSORY": "Accessoire",
|
||||
"ARTIFACT_ADDITION": "Artefact Addition",
|
||||
"ARTIFACT_LABEL": "Label d'artefact",
|
||||
"PREHEAT_POLICY": "Politique de préchauffage",
|
||||
"IMMUTABLE_TAG": "Tag immutable",
|
||||
"LOG": "Log",
|
||||
"NOTIFICATION_POLICY": "Notification Policy",
|
||||
"NOTIFICATION_POLICY": "Politique de notification",
|
||||
"QUOTA": "Quota",
|
||||
"BACK": "Back",
|
||||
"NEXT": "Next",
|
||||
"FINISH": "Finish",
|
||||
"BASIC_INFO": "Basic Information",
|
||||
"SELECT_PERMISSIONS": "Select Permissions",
|
||||
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
|
||||
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
|
||||
"SYSTEM_PERMISSIONS": "System Permissions"
|
||||
"BACK": "Retour",
|
||||
"NEXT": "Suivant",
|
||||
"FINISH": "Finir",
|
||||
"BASIC_INFO": "Informations de base",
|
||||
"SELECT_PERMISSIONS": "Selectionner les permissions",
|
||||
"SELECT_SYSTEM_PERMISSIONS": "Selectionner les permissions système",
|
||||
"SELECT_PROJECT_PERMISSIONS": "Selectionner les permissions projet",
|
||||
"SYSTEM_PERMISSIONS": "Permissions système"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"EDIT_BUTTON": "Éditer",
|
||||
|
@ -536,7 +536,7 @@
|
|||
"RESOURCE_TYPE": "Type de ressource"
|
||||
},
|
||||
"REPLICATION": {
|
||||
"PUSH_BASED_ONLY": "Only for the push-based replication",
|
||||
"PUSH_BASED_ONLY": "Uniquement pour la réplication de type push",
|
||||
"YES": "Oui",
|
||||
"SECONDS": "Secondes",
|
||||
"MINUTES": "Minutes",
|
||||
|
@ -645,7 +645,7 @@
|
|||
"CANNOT_EDIT": "La règle de réplication ne peut pas être modifiée lorsqu'elle est activée.",
|
||||
"INVALID_DATE": "Date non valide.",
|
||||
"PLACEHOLDER": "Nous n'avons trouvé aucune règle de réplication !",
|
||||
"JOB_PLACEHOLDER": "Nous n'avons trouvé aucun travail de réplication !",
|
||||
"JOB_PLACEHOLDER": "Nous n'avons trouvé aucune tâche de réplication !",
|
||||
"NO_ENDPOINT_INFO": "Ajoutez d'abord un endpoint",
|
||||
"NO_LABEL_INFO": "Ajoutez d'abord un label",
|
||||
"NO_PROJECT_INFO": "Ce projet n'existe pas",
|
||||
|
@ -705,11 +705,11 @@
|
|||
"BANDWIDTH_TOOLTIP": "Set the maximum network bandwidth for each replication worker. Please pay attention to the number of concurrent executions (max. {{max_job_workers}}). For unlimited bandwidth, please enter -1.",
|
||||
"UNLIMITED": "Illimitée",
|
||||
"UNREACHABLE_SOURCE_REGISTRY": "Échec de connexion au registre source. Veuillez vérifier que le registre source est disponible avant d'éditer cette règle: {{error}}",
|
||||
"CRON_ERROR_TIP": "The 1st field of the cron string must be 0 and the 2nd filed can not be \"*\"",
|
||||
"CRON_ERROR_TIP": "Le 1er champ de la chaîne cron doit être 0 et le 2ème champ ne peut pas être \"*\"",
|
||||
"COPY_BY_CHUNK": "Copier par morceaux",
|
||||
"COPY_BY_CHUNK_TIP": "Spécifie si le blob doit être copié par morceaux. Transférer par morceaux peut augmenter le nombre de requêtes faites à l'API.",
|
||||
"TRIGGER_STOP_SUCCESS": "Déclenchement avec succès de l'arrêt d'exécution",
|
||||
"CRON_STR": "Cron String"
|
||||
"CRON_STR": "Chaîne cron"
|
||||
},
|
||||
"DESTINATION": {
|
||||
"NEW_ENDPOINT": "Nouveau Endpoint",
|
||||
|
@ -777,6 +777,7 @@
|
|||
"ARTIFACTS": "Artefacts",
|
||||
"SIZE": "Taille",
|
||||
"VULNERABILITY": "Vulnérabilité",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Historique de construction",
|
||||
"SIGNED": "Signé",
|
||||
"AUTHOR": "Auteur",
|
||||
|
@ -937,7 +938,7 @@
|
|||
"TOKEN_REVIEW": "Endpoint de revue de token",
|
||||
"SKIP_SEARCH": "Passer la recherche",
|
||||
"VERIFY_CERT": "Vérifier le certificat",
|
||||
"ADMIN_GROUPS": "Admin Groups"
|
||||
"ADMIN_GROUPS": "Groupes admin"
|
||||
},
|
||||
"OIDC": {
|
||||
"OIDC_PROVIDER": "Fournisseur OIDC",
|
||||
|
@ -1026,6 +1027,41 @@
|
|||
"IN_PROGRESS": "Recherche...",
|
||||
"BACK": "Retour"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Non analysé",
|
||||
|
@ -1043,7 +1079,7 @@
|
|||
"COLUMN_VERSION": "Version Actuelle",
|
||||
"COLUMN_FIXED": "Réparé dans la version",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_ITEMS": "Entrées",
|
||||
"FOOT_OF": "sur",
|
||||
"IN_ALLOW_LIST": "Présent dans la liste blanche CVE",
|
||||
"CVSS3": "CVSS3"
|
||||
|
@ -1090,7 +1126,7 @@
|
|||
"TAG_COMMAND": "Taguer une image pour ce projet :",
|
||||
"PUSH_COMMAND": "Push une image dans ce projet :",
|
||||
"COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement les commandes de référence.",
|
||||
"COPY_PULL_COMMAND": "COPY PULL COMMAND"
|
||||
"COPY_PULL_COMMAND": "COMMANDE COPY PULL"
|
||||
},
|
||||
"ARTIFACT": {
|
||||
"FILTER_FOR_ARTIFACTS": "Filtrer les artefacts",
|
||||
|
@ -1106,6 +1142,7 @@
|
|||
"ALL": "Tous",
|
||||
"PLACEHOLDER": "Nous n'avons trouvé aucun artefact !",
|
||||
"SCAN_UNSUPPORTED": "Non supporté",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Résumé",
|
||||
"DEPENDENCIES": "Dépendances",
|
||||
"VALUES": "Valeurs",
|
||||
|
@ -1144,7 +1181,7 @@
|
|||
"PULL_TIME": "Date/Heure de pull",
|
||||
"PUSH_TIME": "Date/Heure de push",
|
||||
"OF": "sur",
|
||||
"ITEMS": "items",
|
||||
"ITEMS": "entrées",
|
||||
"ADD_TAG": "AJOUTER TAG",
|
||||
"REMOVE_TAG": "SUPPRIMER TAG",
|
||||
"NAME_ALREADY_EXISTS": "Ce tag existe déjà dans ce dépôt"
|
||||
|
@ -1174,14 +1211,14 @@
|
|||
"DELETE": "Supprimer",
|
||||
"OF": "sur",
|
||||
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "Nombre par défaut d'artefacts par projet",
|
||||
"PROJECT_QUOTA_DEFAULT_DISK": "Default quota space per project",
|
||||
"PROJECT_QUOTA_DEFAULT_DISK": "Quota d'espace par défaut par projet",
|
||||
"EDIT_PROJECT_QUOTAS": "Éditer les quotas projet",
|
||||
"EDIT_DEFAULT_PROJECT_QUOTAS": "Éditer les quotas projet par défaut",
|
||||
"SET_QUOTAS": "Configurer les quotas pour le projet '{{params}}'",
|
||||
"SET_DEFAULT_QUOTAS": "Configurer les quotas projet par défaut lors de la création de nouveaux projets",
|
||||
"COUNT_QUOTA": "Quota de nombre",
|
||||
"COUNT_DEFAULT_QUOTA": "Quota de nombre par défaut",
|
||||
"STORAGE_QUOTA": "Project quota limits",
|
||||
"STORAGE_QUOTA": "Limites des quotas de projets",
|
||||
"STORAGE_DEFAULT_QUOTA": "Quota de stockage par défaut",
|
||||
"SAVE_SUCCESS": "Edition de quota effectuée",
|
||||
"UNLIMITED": "Illimité",
|
||||
|
@ -1432,9 +1469,9 @@
|
|||
"NAME_REX": "Le nom doit comporter au moins 2 caractères avec des minuscules, des chiffres et. _- et doit commencer par des caractères ou des chiffres.",
|
||||
"DESCRIPTION": "Description",
|
||||
"SBOM": "SBOM",
|
||||
"VULNERABILITY": "Vulnerability",
|
||||
"SUPPORTED": "Supported",
|
||||
"NOT_SUPPORTED": "Not Supported",
|
||||
"VULNERABILITY": "Vulnérabilité",
|
||||
"SUPPORTED": "Supporté",
|
||||
"NOT_SUPPORTED": "Non Supporté",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "L'URL de l'endpoint existe déjà",
|
||||
"ENDPOINT_REQUIRED": "L'URL de l'endpoint est requise",
|
||||
|
@ -1639,7 +1676,7 @@
|
|||
"PREHEAT_EXPLAIN": "Le préchauffage migrera l'image vers le réseau p2p",
|
||||
"CRITERIA_EXPLAIN": "Comme spécifié dans la section 'Sécurité de déploiement' dans l'onglet Configuration",
|
||||
"SKIP_CERT_VERIFY": "Cochez cette case pour ignorer la vérification du certificat lorsque le fournisseur distant utilise un certificat auto-signé ou non approuvé.",
|
||||
"NAME_TOOLTIP": "Policy name consists of one or more groups of uppercase letter, lowercase letter or number; and groups are separated by a dot, underscore, or hyphen.",
|
||||
"NAME_TOOLTIP": "Le nom de la politique consiste en un ou plusieurs groupes de lettres (majuscules ou minuscules) ou de chiffres ; les groupes sont séparés par un point, un trait de soulignement ou un trait d'union.",
|
||||
"NEED_HELP": "Veuillez d'abord demander à votre administrateur système d'ajouter un fournisseur"
|
||||
},
|
||||
"PAGINATION": {
|
||||
|
@ -1675,9 +1712,9 @@
|
|||
"PROJECTS_MODAL_TITLE": "Projets pour le compte robot",
|
||||
"PROJECTS_MODAL_SUMMARY": "Voici les projets couverts par ce compte robot.",
|
||||
"CREATE_ROBOT": "Créer un compte robot Système",
|
||||
"CREATE_ROBOT_SUMMARY": "Create a system Robot Account that will cover permissions for the system as well as for specific projects",
|
||||
"CREATE_ROBOT_SUMMARY": "Créer un compte système Robot qui couvrira les autorisations pour le système ainsi que pour des projets spécifiques",
|
||||
"EDIT_ROBOT": "Éditer un compte robot Système",
|
||||
"EDIT_ROBOT_SUMMARY": "Edit a system Robot Account that will cover permissions for the system as well as for specific projects",
|
||||
"EDIT_ROBOT_SUMMARY": "Éditer un compte système Robot qui couvrira les autorisations pour le système ainsi que pour des projets spécifiques",
|
||||
"EXPIRATION_TIME": "Date/Heure d'Expiration",
|
||||
"EXPIRATION_TIME_EXPLAIN": "L'heure d'expiration (en jours, le point de départ est l'heure de création) du jeton du compte robot. Pour ne jamais expirer, entrer \"-1\".",
|
||||
"EXPIRATION_DEFAULT": "jours (défaut)",
|
||||
|
@ -1687,7 +1724,7 @@
|
|||
"COVER_ALL": "Couvrir tous les projets",
|
||||
"COVER_ALL_EXPLAIN": "Cocher pour appliquer à tous les projets existants et futurs",
|
||||
"COVER_ALL_SUMMARY": "\"Tous les projets existants et futurs\" sélectionné.",
|
||||
"RESET_PERMISSION": "RESET ALL PROJECT PERMISSIONS",
|
||||
"RESET_PERMISSION": "REINITIALISER TOUTES LES PERMISSIONS PROJET",
|
||||
"PERMISSION_COLUMN": "Permissions",
|
||||
"EXPIRES_AT": "Expire à",
|
||||
"VIEW_SECRET": "Actualiser le secret",
|
||||
|
@ -1720,8 +1757,8 @@
|
|||
"REPOSITORY": "Dépôt",
|
||||
"EXPIRES_IN": "Expire dans",
|
||||
"EXPIRED": "Expiré",
|
||||
"SELECT_ALL_PROJECT": "SELECT ALL PROJECTS",
|
||||
"UNSELECT_ALL_PROJECT": "UNSELECT ALL PROJECTS"
|
||||
"SELECT_ALL_PROJECT": "SELECTIONNER TOUS LES PROJETS",
|
||||
"UNSELECT_ALL_PROJECT": "DESELECTIONNER TOUS LES PROJETS"
|
||||
},
|
||||
"ACCESSORY": {
|
||||
"DELETION_TITLE_ACCESSORY": "Confirmer la suppression de l'accessoire",
|
||||
|
@ -1772,7 +1809,7 @@
|
|||
"EXPORT_TITLE": "Export de CVEs",
|
||||
"EXPORT_SUBTITLE": "Définir les conditions d'exportation",
|
||||
"EXPORT_CVE_FILTER_HELP_TEXT": "Entrer plusieurs cveIDs séparés par des virgules",
|
||||
"CVE_IDS": "CVE IDs",
|
||||
"CVE_IDS": "IDs CVE",
|
||||
"EXPORT_BUTTON": "Exporter",
|
||||
"JOB_NAME": "Nom de la tâche",
|
||||
"JOB_NAME_REQUIRED": "Le nom de la tâche est requis",
|
||||
|
@ -1855,9 +1892,9 @@
|
|||
"SCHEDULE_RESUME_BTN_INFO": "REPRENDRE — Reprend les files d'attente de tâches à exécuter.",
|
||||
"WORKER_FREE_BTN_INFO": "Arrête les tâches en cours pour libérer le worker",
|
||||
"CRON": "Cron",
|
||||
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
|
||||
"WAITING_TOO_LONG_2": "dashboard.",
|
||||
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
|
||||
"WAITING_TOO_LONG_1": "Certaines tâches sont en attente d'exécution depuis plus de 24 heures. Veuillez vérifier le ",
|
||||
"WAITING_TOO_LONG_2": "tableau de bord.",
|
||||
"WAITING_TOO_LONG_3": "Pour plus de détails, veuillez consulter le ",
|
||||
"WAITING_TOO_LONG_4": "Wiki."
|
||||
},
|
||||
"CLARITY": {
|
||||
|
@ -1865,8 +1902,8 @@
|
|||
"CLOSE": "Fermer",
|
||||
"SHOW": "Afficher",
|
||||
"HIDE": "Cacher",
|
||||
"EXPAND": "Etendre",
|
||||
"COLLAPSE": "Collapse",
|
||||
"EXPAND": "Déplier",
|
||||
"COLLAPSE": "Replier",
|
||||
"MORE": "Plus",
|
||||
"SELECT": "Sélectionner",
|
||||
"SELECT_ALL": "Tout sélectionner",
|
||||
|
@ -1923,7 +1960,7 @@
|
|||
"ENTER_MESSAGE": "Entrer votre message ici"
|
||||
},
|
||||
"SECURITY_HUB": {
|
||||
"SECURITY_HUB": "Tableau de bord de sécurité",
|
||||
"SECURITY_HUB": "Centre de sécurité",
|
||||
"ARTIFACTS": "artefact(s)",
|
||||
"SCANNED": "scannés",
|
||||
"NOT_SCANNED": "non scannés",
|
||||
|
@ -1931,7 +1968,7 @@
|
|||
"TOTAL_AND_FIXABLE": "{{totalNum}} total dont {{fixableNum}} corrigeables",
|
||||
"TOP_5_ARTIFACT": "Top 5 des Artefacts les plus Dangereux",
|
||||
"TOP_5_CVE": "Top 5 des CVEs les plus Dangereuses",
|
||||
"CVE_ID": "CVE ID",
|
||||
"CVE_ID": "ID CVE",
|
||||
"VUL": "Vulnérabilités",
|
||||
"CVE": "CVEs",
|
||||
"FILTER_BY": "Filtrer par",
|
||||
|
|
|
@ -775,6 +775,7 @@
|
|||
"ARTIFACTS": "아티팩트들",
|
||||
"SIZE": "크기",
|
||||
"VULNERABILITY": "취약점",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "기록 생성",
|
||||
"SIGNED": "서명됨",
|
||||
"AUTHOR": "작성자",
|
||||
|
@ -1025,6 +1026,41 @@
|
|||
"IN_PROGRESS": "검색 중...",
|
||||
"BACK": "뒤로"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "스캔되지 않음",
|
||||
|
@ -1105,6 +1141,7 @@
|
|||
"ALL": "모두",
|
||||
"PLACEHOLDER": "아티팩트를 찾을 수 없음!",
|
||||
"SCAN_UNSUPPORTED": "지원되지 않음",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "요약",
|
||||
"DEPENDENCIES": "종속성",
|
||||
"VALUES": "값",
|
||||
|
|
|
@ -776,6 +776,7 @@
|
|||
"ARTIFACTS": "Artefatos",
|
||||
"SIZE": "Tamanho",
|
||||
"VULNERABILITY": "Vulnerabilidade",
|
||||
"SBOM": "SBOM",
|
||||
"SIGNED": "Assinada",
|
||||
"AUTHOR": "Autor",
|
||||
"CREATED": "Data de criação",
|
||||
|
@ -1024,6 +1025,41 @@
|
|||
"IN_PROGRESS": "Buscando...",
|
||||
"BACK": "Voltar"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Não analisado",
|
||||
|
@ -1104,6 +1140,7 @@
|
|||
"ALL": "Todos",
|
||||
"PLACEHOLDER": "Nenhum artefato encontrado!",
|
||||
"SCAN_UNSUPPORTED": "Não pode ser examinada",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Resumo",
|
||||
"DEPENDENCIES": "Dependências",
|
||||
"VALUES": "Valores",
|
||||
|
|
|
@ -777,6 +777,7 @@
|
|||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Boyut",
|
||||
"VULNERABILITY": "Güvenlik Açığı",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Geçmişi Oluştur",
|
||||
"SIGNED": "İmzalanmış",
|
||||
"AUTHOR": "Yazar",
|
||||
|
@ -1027,6 +1028,41 @@
|
|||
"IN_PROGRESS": "Ara...",
|
||||
"BACK": "Geri"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Taranmadı",
|
||||
|
@ -1107,6 +1143,7 @@
|
|||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Özet",
|
||||
"DEPENDENCIES": "Bağımlılıklar",
|
||||
"VALUES": "Değerler",
|
||||
|
|
|
@ -776,6 +776,7 @@
|
|||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "大小",
|
||||
"VULNERABILITY": "漏洞",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "构建历史",
|
||||
"SIGNED": "已签名",
|
||||
"AUTHOR": "作者",
|
||||
|
@ -1025,6 +1026,41 @@
|
|||
"IN_PROGRESS": "搜索中...",
|
||||
"BACK": "返回"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "未扫描",
|
||||
|
@ -1105,6 +1141,7 @@
|
|||
"ALL": "所有",
|
||||
"PLACEHOLDER": "未发现任何 artifacts!",
|
||||
"SCAN_UNSUPPORTED": "不支持扫描",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "概要",
|
||||
"DEPENDENCIES": "依赖",
|
||||
"VALUES": "取值",
|
||||
|
|
|
@ -776,6 +776,7 @@
|
|||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "大小",
|
||||
"VULNERABILITY": "弱點",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "建置歷史",
|
||||
"SIGNED": "已簽署",
|
||||
"AUTHOR": "作者",
|
||||
|
@ -1024,6 +1025,41 @@
|
|||
"IN_PROGRESS": "搜尋中...",
|
||||
"BACK": "返回"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "未掃描",
|
||||
|
@ -1104,6 +1140,7 @@
|
|||
"ALL": "全部",
|
||||
"PLACEHOLDER": "未發現任何 artifacts!",
|
||||
"SCAN_UNSUPPORTED": "不支援掃描",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "摘要",
|
||||
"DEPENDENCIES": "相依性",
|
||||
"VALUES": "值",
|
||||
|
|
|
@ -65,6 +65,38 @@ var (
|
|||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
// cosign adopt oci-spec 1.1 will have request and manifest like below
|
||||
// It will skip this middleware since not using cosignRe for subject artifact reference
|
||||
// use Subject Middleware indtead
|
||||
/*
|
||||
PUT /v2/library/goharbor/harbor-db/manifests/sha256:aabea2bdd5a6fb79c13837b88c7b158f4aa57a621194ee21959d0b520eda412f
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.dev.cosign.artifact.sig.v1+json",
|
||||
"size": 233,
|
||||
"digest": "sha256:c025e9532dbc880534be96dbbb86a6bf63a272faced7f07bb8b4ceb45ca938d1"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
|
||||
"size": 257,
|
||||
"digest": "sha256:38d07d81bf1d026da6420295113115d999ad6da90073b5e67147f978626423e6",
|
||||
"annotations": {
|
||||
"dev.cosignproject.cosign/signature": "MEUCIDOQc6I4MSd4/s8Bc8S7LXHCOnm4MGimpQdeCInLzM0VAiEAhWWYxmwEmYrFJ8xYNE3ow7PS4zeGe1R4RUbXRIawKJ4=",
|
||||
"dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEUCIC5DSFQx3nZhPFquF4NAdfetjqLR6qAa9i04cEtAg7VjAiEAzG2DUxqH+MdFSPih/EL/Vvsn3L1xCJUlOmRZeUYZaG0=\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzOGQwN2Q4MWJmMWQwMjZkYTY0MjAyOTUxMTMxMTVkOTk5YWQ2ZGE5MDA3M2I1ZTY3MTQ3Zjk3ODYyNjQyM2U2In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRE9RYzZJNE1TZDQvczhCYzhTN0xYSENPbm00TUdpbXBRZGVDSW5Mek0wVkFpRUFoV1dZeG13RW1ZckZKOHhZTkUzb3c3UFM0emVHZTFSNFJVYlhSSWF3S0o0PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWVVoSk1DOTZiWEpIYW1VNE9FeFVTM0ZDU2tvNWJXZDNhWEprWkFwaVJrZGpNQzlRYWtWUUwxbFJNelJwZFZweWJGVnRhMGx3ZDBocFdVTmxSV3M0YWpoWE5rSnBaV3BxTHk5WmVVRnZZaXN5VTFCTGRqUkJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=\",\"integratedTime\":1712651102,\"logIndex\":84313668,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 2621,
|
||||
"digest": "sha256:e50f88df1b11f94627e35bed9f34214392363508a2b07146d0a94516da97e4c0"
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
func SignatureMiddleware() func(http.Handler) http.Handler {
|
||||
return middleware.AfterResponse(func(w http.ResponseWriter, r *http.Request, statusCode int) error {
|
||||
|
|
|
@ -39,6 +39,9 @@ var (
|
|||
// the media type of notation signature layer
|
||||
mediaTypeNotationLayer = "application/vnd.cncf.notary.signature"
|
||||
|
||||
// cosign media type in config layer, which would support in oci-spec1.1
|
||||
mediaTypeCosignConfig = "application/vnd.dev.cosign.artifact.sig.v1+json"
|
||||
|
||||
// annotation of nydus image
|
||||
layerAnnotationNydusBootstrap = "containerd.io/snapshot/nydus-bootstrap"
|
||||
|
||||
|
@ -152,6 +155,8 @@ func Middleware() func(http.Handler) http.Handler {
|
|||
}
|
||||
case mediaTypeNotationLayer:
|
||||
accData.Type = model.TypeNotationSignature
|
||||
case mediaTypeCosignConfig:
|
||||
accData.Type = model.TypeCosignSignature
|
||||
case mediaTypeHarborSBOM:
|
||||
accData.Type = model.TypeHarborSBOM
|
||||
}
|
||||
|
|
|
@ -22,11 +22,16 @@ import (
|
|||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/cache"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
lib_http "github.com/goharbor/harbor/src/lib/http"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/accessory"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/cached/manifest/redis"
|
||||
"github.com/goharbor/harbor/src/pkg/registry"
|
||||
"github.com/goharbor/harbor/src/server/router"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/handler"
|
||||
)
|
||||
|
@ -38,12 +43,16 @@ func newReferrersHandler() http.Handler {
|
|||
return &referrersHandler{
|
||||
artifactManager: artifact.NewManager(),
|
||||
accessoryManager: accessory.NewManager(),
|
||||
registryClient: registry.Cli,
|
||||
maniCacheManager: redis.NewManager(),
|
||||
}
|
||||
}
|
||||
|
||||
type referrersHandler struct {
|
||||
artifactManager artifact.Manager
|
||||
accessoryManager accessory.Manager
|
||||
registryClient registry.Client
|
||||
maniCacheManager redis.CachedManager
|
||||
}
|
||||
|
||||
func (r *referrersHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -75,18 +84,56 @@ func (r *referrersHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
lib_http.SendError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build index manifest from accessories
|
||||
mfs := make([]ocispec.Descriptor, 0)
|
||||
for _, acc := range accs {
|
||||
accArt, err := r.artifactManager.GetByDigest(ctx, repository, acc.GetData().Digest)
|
||||
accArtDigest := acc.GetData().Digest
|
||||
accArt, err := r.artifactManager.GetByDigest(ctx, repository, accArtDigest)
|
||||
if err != nil {
|
||||
lib_http.SendError(w, err)
|
||||
return
|
||||
}
|
||||
mf := ocispec.Descriptor{
|
||||
// whether get manifest from cache
|
||||
fromCache := false
|
||||
// whether need write manifest to cache
|
||||
writeCache := false
|
||||
var maniContent []byte
|
||||
|
||||
// pull manifest, will try to pull from cache first
|
||||
// and write to cache when pull manifest from registry at first time
|
||||
if config.CacheEnabled() {
|
||||
maniContent, err = r.maniCacheManager.Get(req.Context(), accArtDigest)
|
||||
if err == nil {
|
||||
fromCache = true
|
||||
} else {
|
||||
log.Debugf("failed to get manifest %s from cache, will fallback to registry, error: %v", accArtDigest, err)
|
||||
if errors.As(err, &cache.ErrNotFound) {
|
||||
writeCache = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !fromCache {
|
||||
mani, _, err := r.registryClient.PullManifest(accArt.RepositoryName, accArtDigest)
|
||||
if err != nil {
|
||||
lib_http.SendError(w, err)
|
||||
return
|
||||
}
|
||||
_, maniContent, err = mani.Payload()
|
||||
if err != nil {
|
||||
lib_http.SendError(w, err)
|
||||
return
|
||||
}
|
||||
// write manifest to cache when first time pulling
|
||||
if writeCache {
|
||||
err = r.maniCacheManager.Save(req.Context(), accArtDigest, maniContent)
|
||||
if err != nil {
|
||||
log.Warningf("failed to save accArt manifest %s to cache, error: %v", accArtDigest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
desc := ocispec.Descriptor{
|
||||
MediaType: accArt.ManifestMediaType,
|
||||
Size: accArt.Size,
|
||||
Size: int64(len(maniContent)),
|
||||
Digest: digest.Digest(accArt.Digest),
|
||||
Annotations: accArt.Annotations,
|
||||
ArtifactType: accArt.ArtifactType,
|
||||
|
@ -94,10 +141,10 @@ func (r *referrersHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
// filter use accArt.ArtifactType as artifactType
|
||||
if at != "" {
|
||||
if accArt.ArtifactType == at {
|
||||
mfs = append(mfs, mf)
|
||||
mfs = append(mfs, desc)
|
||||
}
|
||||
} else {
|
||||
mfs = append(mfs, mf)
|
||||
mfs = append(mfs, desc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,20 +3,50 @@ package registry
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
beegocontext "github.com/beego/beego/v2/server/web/context"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/cache"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
accessorymodel "github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
basemodel "github.com/goharbor/harbor/src/pkg/accessory/model/base"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/distribution"
|
||||
"github.com/goharbor/harbor/src/server/router"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
|
||||
arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
|
||||
testmanifest "github.com/goharbor/harbor/src/testing/pkg/cached/manifest/redis"
|
||||
regtesting "github.com/goharbor/harbor/src/testing/pkg/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
OCIManifest = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.example.sbom",
|
||||
"digest": "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
|
||||
"size": 123
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.example.data.v1.tar+gzip",
|
||||
"digest": "sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317",
|
||||
"size": 1234
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"name": "test-image"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
func TestReferrersHandlerOK(t *testing.T) {
|
||||
|
@ -35,10 +65,10 @@ func TestReferrersHandlerOK(t *testing.T) {
|
|||
|
||||
artifactMock.On("GetByDigest", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(&artifact.Artifact{
|
||||
Digest: digestVal,
|
||||
Digest: "sha256:4911bb745e19a6b5513755f3d033f10ef10c34b40edc631809e28be8a7c005f6",
|
||||
ManifestMediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
MediaType: "application/vnd.example.sbom",
|
||||
ArtifactType: "application/vnd.example+type",
|
||||
ArtifactType: "application/vnd.example.sbom",
|
||||
Size: 1000,
|
||||
Annotations: map[string]string{
|
||||
"name": "test-image",
|
||||
|
@ -56,13 +86,23 @@ func TestReferrersHandlerOK(t *testing.T) {
|
|||
SubArtifactDigest: digestVal,
|
||||
SubArtifactRepo: "goharbor",
|
||||
Type: accessorymodel.TypeCosignSignature,
|
||||
Digest: "sha256:4911bb745e19a6b5513755f3d033f10ef10c34b40edc631809e28be8a7c005f6",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(OCIManifest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
regCliMock := ®testing.Client{}
|
||||
config.DefaultMgr().Set(context.TODO(), "cache_enabled", false)
|
||||
mock.OnAnything(regCliMock, "PullManifest").Return(manifest, "", nil)
|
||||
|
||||
handler := &referrersHandler{
|
||||
artifactManager: artifactMock,
|
||||
accessoryManager: accessoryMock,
|
||||
registryClient: regCliMock,
|
||||
}
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
@ -72,10 +112,89 @@ func TestReferrersHandlerOK(t *testing.T) {
|
|||
t.Errorf("Expected status code %d, but got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
index := &ocispec.Index{}
|
||||
json.Unmarshal([]byte(rec.Body.String()), index)
|
||||
if index.Manifests[0].ArtifactType != "application/vnd.example+type" {
|
||||
t.Errorf("Expected response body %s, but got %s", "application/vnd.example+type", rec.Body.String())
|
||||
json.Unmarshal(rec.Body.Bytes(), index)
|
||||
if index.Manifests[0].ArtifactType != "application/vnd.example.sbom" {
|
||||
t.Errorf("Expected response body %s, but got %s", "application/vnd.example.sbom", rec.Body.String())
|
||||
}
|
||||
_, content, _ := manifest.Payload()
|
||||
assert.Equal(t, int64(len(content)), index.Manifests[0].Size)
|
||||
}
|
||||
|
||||
func TestReferrersHandlerSavetoCache(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
digestVal := "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
|
||||
req, err := http.NewRequest("GET", "/v2/test/repository/referrers/sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input := &beegocontext.BeegoInput{}
|
||||
input.SetParam(":reference", digestVal)
|
||||
*req = *(req.WithContext(context.WithValue(req.Context(), router.ContextKeyInput{}, input)))
|
||||
|
||||
artifactMock := &arttesting.Manager{}
|
||||
accessoryMock := &accessorytesting.Manager{}
|
||||
|
||||
artifactMock.On("GetByDigest", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(&artifact.Artifact{
|
||||
Digest: "sha256:4911bb745e19a6b5513755f3d033f10ef10c34b40edc631809e28be8a7c005f6",
|
||||
ManifestMediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
MediaType: "application/vnd.example.sbom",
|
||||
ArtifactType: "application/vnd.example.sbom",
|
||||
Size: 1000,
|
||||
Annotations: map[string]string{
|
||||
"name": "test-image",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
accessoryMock.On("Count", mock.Anything, mock.Anything).
|
||||
Return(int64(1), nil)
|
||||
accessoryMock.On("List", mock.Anything, mock.Anything).
|
||||
Return([]accessorymodel.Accessory{
|
||||
&basemodel.Default{
|
||||
Data: accessorymodel.AccessoryData{
|
||||
ID: 1,
|
||||
ArtifactID: 2,
|
||||
SubArtifactDigest: digestVal,
|
||||
SubArtifactRepo: "goharbor",
|
||||
Type: accessorymodel.TypeCosignSignature,
|
||||
Digest: "sha256:4911bb745e19a6b5513755f3d033f10ef10c34b40edc631809e28be8a7c005f6",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, []byte(OCIManifest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cache_enabled pull from cahce
|
||||
config.DefaultMgr().Set(context.TODO(), "cache_enabled", true)
|
||||
cacheManagerMock := &testmanifest.CachedManager{}
|
||||
mock.OnAnything(cacheManagerMock, "Get").Return(nil, fmt.Errorf("unable to do stuff: %w", cache.ErrNotFound))
|
||||
regCliMock := ®testing.Client{}
|
||||
mock.OnAnything(regCliMock, "PullManifest").Return(manifest, "", nil)
|
||||
mock.OnAnything(cacheManagerMock, "Save").Return(nil)
|
||||
|
||||
handler := &referrersHandler{
|
||||
artifactManager: artifactMock,
|
||||
accessoryManager: accessoryMock,
|
||||
registryClient: regCliMock,
|
||||
maniCacheManager: cacheManagerMock,
|
||||
}
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
// check that the response has the expected status code (200 OK)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, but got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
index := &ocispec.Index{}
|
||||
json.Unmarshal(rec.Body.Bytes(), index)
|
||||
if index.Manifests[0].ArtifactType != "application/vnd.example.sbom" {
|
||||
t.Errorf("Expected response body %s, but got %s", "application/vnd.example.sbom", rec.Body.String())
|
||||
}
|
||||
_, content, _ := manifest.Payload()
|
||||
assert.Equal(t, int64(len(content)), index.Manifests[0].Size)
|
||||
}
|
||||
|
||||
func TestReferrersHandlerEmpty(t *testing.T) {
|
||||
|
|
|
@ -107,8 +107,8 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr
|
|||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
assembler := assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities))
|
||||
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
|
||||
assembler := assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities))
|
||||
var artifacts []*models.Artifact
|
||||
for _, art := range arts {
|
||||
artifact := &model.Artifact{}
|
||||
|
@ -138,8 +138,9 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif
|
|||
}
|
||||
art := &model.Artifact{}
|
||||
art.Artifact = *artifact
|
||||
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
|
||||
|
||||
err = assembler.NewVulAssembler(lib.BoolValue(params.WithScanOverview), parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
|
||||
err = assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("failed to assemble vulnerabilities with artifact, error: %v", err)
|
||||
}
|
||||
|
|
|
@ -20,43 +20,48 @@ import (
|
|||
"github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||
)
|
||||
|
||||
const (
|
||||
vulnerabilitiesAddition = "vulnerabilities"
|
||||
startTime = "start_time"
|
||||
endTime = "end_time"
|
||||
scanStatus = "scan_status"
|
||||
sbomDigest = "sbom_digest"
|
||||
duration = "duration"
|
||||
)
|
||||
|
||||
// NewVulAssembler returns vul assembler
|
||||
func NewVulAssembler(withScanOverview bool, mimeTypes []string) *VulAssembler {
|
||||
return &VulAssembler{
|
||||
scanChecker: scan.NewChecker(),
|
||||
scanCtl: scan.DefaultController,
|
||||
|
||||
withScanOverview: withScanOverview,
|
||||
mimeTypes: mimeTypes,
|
||||
// NewScanReportAssembler returns vul assembler
|
||||
func NewScanReportAssembler(option *model.OverviewOptions, mimeTypes []string) *ScanReportAssembler {
|
||||
return &ScanReportAssembler{
|
||||
overviewOption: option,
|
||||
scanChecker: scan.NewChecker(),
|
||||
scanCtl: scan.DefaultController,
|
||||
mimeTypes: mimeTypes,
|
||||
}
|
||||
}
|
||||
|
||||
// VulAssembler vul assembler
|
||||
type VulAssembler struct {
|
||||
// ScanReportAssembler vul assembler
|
||||
type ScanReportAssembler struct {
|
||||
scanChecker scan.Checker
|
||||
scanCtl scan.Controller
|
||||
|
||||
artifacts []*model.Artifact
|
||||
withScanOverview bool
|
||||
mimeTypes []string
|
||||
artifacts []*model.Artifact
|
||||
mimeTypes []string
|
||||
overviewOption *model.OverviewOptions
|
||||
}
|
||||
|
||||
// WithArtifacts set artifacts for the assembler
|
||||
func (assembler *VulAssembler) WithArtifacts(artifacts ...*model.Artifact) *VulAssembler {
|
||||
func (assembler *ScanReportAssembler) WithArtifacts(artifacts ...*model.Artifact) *ScanReportAssembler {
|
||||
assembler.artifacts = artifacts
|
||||
|
||||
return assembler
|
||||
}
|
||||
|
||||
// Assemble assemble vul for the artifacts
|
||||
func (assembler *VulAssembler) Assemble(ctx context.Context) error {
|
||||
func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error {
|
||||
version := lib.GetAPIVersion(ctx)
|
||||
|
||||
for _, artifact := range assembler.artifacts {
|
||||
|
@ -72,7 +77,7 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
|
|||
|
||||
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
|
||||
|
||||
if assembler.withScanOverview {
|
||||
if assembler.overviewOption.WithVuln {
|
||||
for _, mimeType := range assembler.mimeTypes {
|
||||
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{mimeType})
|
||||
if err != nil {
|
||||
|
@ -83,6 +88,20 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
if assembler.overviewOption.WithSBOM {
|
||||
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeSBOMReport})
|
||||
if err != nil {
|
||||
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, v1.MimeTypeSBOMReport, err)
|
||||
} else if len(overview) > 0 {
|
||||
artifact.SBOMOverView = map[string]interface{}{
|
||||
startTime: overview[startTime],
|
||||
endTime: overview[endTime],
|
||||
scanStatus: overview[scanStatus],
|
||||
sbomDigest: overview[sbomDigest],
|
||||
duration: overview[duration],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
|
@ -20,6 +20,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
v1sq "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||
"github.com/goharbor/harbor/src/testing/controller/scan"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
|
@ -33,11 +34,11 @@ func (suite *VulAssemblerTestSuite) TestScannable() {
|
|||
checker := &scan.Checker{}
|
||||
scanCtl := &scan.Controller{}
|
||||
|
||||
assembler := VulAssembler{
|
||||
scanChecker: checker,
|
||||
scanCtl: scanCtl,
|
||||
withScanOverview: true,
|
||||
mimeTypes: []string{"mimeType"},
|
||||
assembler := ScanReportAssembler{
|
||||
scanChecker: checker,
|
||||
scanCtl: scanCtl,
|
||||
overviewOption: model.NewOverviewOptions(model.WithVuln(true)),
|
||||
mimeTypes: []string{"mimeType"},
|
||||
}
|
||||
|
||||
mock.OnAnything(checker, "IsScannable").Return(true, nil)
|
||||
|
@ -56,10 +57,10 @@ func (suite *VulAssemblerTestSuite) TestNotScannable() {
|
|||
checker := &scan.Checker{}
|
||||
scanCtl := &scan.Controller{}
|
||||
|
||||
assembler := VulAssembler{
|
||||
scanChecker: checker,
|
||||
scanCtl: scanCtl,
|
||||
withScanOverview: true,
|
||||
assembler := ScanReportAssembler{
|
||||
scanChecker: checker,
|
||||
scanCtl: scanCtl,
|
||||
overviewOption: model.NewOverviewOptions(model.WithVuln(true)),
|
||||
}
|
||||
|
||||
mock.OnAnything(checker, "IsScannable").Return(false, nil)
|
||||
|
@ -74,6 +75,32 @@ func (suite *VulAssemblerTestSuite) TestNotScannable() {
|
|||
scanCtl.AssertNotCalled(suite.T(), "GetSummary")
|
||||
}
|
||||
|
||||
func (suite *VulAssemblerTestSuite) TestAssembleSBOMOverview() {
|
||||
checker := &scan.Checker{}
|
||||
scanCtl := &scan.Controller{}
|
||||
|
||||
assembler := ScanReportAssembler{
|
||||
scanChecker: checker,
|
||||
scanCtl: scanCtl,
|
||||
overviewOption: model.NewOverviewOptions(model.WithSBOM(true)),
|
||||
mimeTypes: []string{v1sq.MimeTypeSBOMReport},
|
||||
}
|
||||
|
||||
mock.OnAnything(checker, "IsScannable").Return(true, nil)
|
||||
overview := map[string]interface{}{
|
||||
"sbom_digest": "sha256:123456",
|
||||
"scan_status": "Success",
|
||||
}
|
||||
mock.OnAnything(scanCtl, "GetSummary").Return(overview, nil)
|
||||
|
||||
var artifact model.Artifact
|
||||
err := assembler.WithArtifacts(&artifact).Assemble(context.TODO())
|
||||
suite.Nil(err)
|
||||
suite.Equal(artifact.SBOMOverView["sbom_digest"], "sha256:123456")
|
||||
suite.Equal(artifact.SBOMOverView["scan_status"], "Success")
|
||||
|
||||
}
|
||||
|
||||
func TestVulAssemblerTestSuite(t *testing.T) {
|
||||
suite.Run(t, &VulAssemblerTestSuite{})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue