Merge branch 'main' into dependabot/npm_and_yarn/src/portal/multi-a1e856e0dc

This commit is contained in:
Shengwen YU 2024-04-15 15:32:22 +08:00 committed by GitHub
commit c550259ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
204 changed files with 6465 additions and 414 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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) .

View File

@ -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 ]

BIN
icons/sbom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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, "//"))
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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 = &registry.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{})
}

View File

@ -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

View File

@ -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"))
}

View File

@ -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{

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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")
}

View File

@ -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{}))
}

View File

@ -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
//

View File

@ -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
}

View File

@ -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"

View File

@ -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

View File

@ -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() {

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -27,4 +27,5 @@ const (
DigestOfIconAccCosign = "sha256:20401d5b3a0f6dbc607c8d732eb08471af4ae6b19811a4efce8c6a724aed2882"
DigestOfIconAccNotation = "sha256:3ac706e102bbe9362b400aa162df58135d35e66b9c3bee2165de92022d25fe34"
DigestOfIconAccNydus = "sha256:dfcb6617cd9c144358dc1b305b87bbe34f0b619f1e329116e6aee2e41f2e34cf"
DigestOfIconAccSBOM = "sha256:c19f80c357cd7e90d2a01b9ae3e2eb62ce447a2662bb590a19177d72d550bdae"
)

View File

@ -33,6 +33,7 @@ var (
model.TypeCosignSignature: icon.DigestOfIconAccCosign,
model.TypeNotationSignature: icon.DigestOfIconAccNotation,
model.TypeNydusAccelerator: icon.DigestOfIconAccNydus,
model.TypeHarborSBOM: icon.DigestOfIconAccSBOM,
}
)

View File

@ -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
}

44
src/pkg/scan/handler.go Normal file
View File

@ -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)
}

View File

@ -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]

View File

@ -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)
}

View File

@ -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

View File

@ -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")
}

View File

@ -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"
)

View File

@ -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{

View File

@ -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{

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
})

View File

@ -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...)

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -18,4 +18,5 @@ export enum ADDITIONS {
SUMMARY = 'readme.md',
VALUES = 'values.yaml',
DEPENDENCIES = 'dependencies',
SBOMS = 'sboms',
}

View File

@ -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();
}
));
});

View File

@ -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)
);

View File

@ -42,6 +42,34 @@
<clr-icon shape="stop" size="16"></clr-icon>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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">

View File

@ -161,6 +161,10 @@
width: 11rem !important;
}
.sbom-column {
width: 6rem !important;
}
.annotations-column {
width: 5rem !important;
}

View File

@ -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) {

View File

@ -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[] = [];

View File

@ -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,

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
});
});
});

View File

@ -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);
}
);
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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}`
);
});
});
});

View File

@ -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,
},
});
}
}
}

View File

@ -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;
}

View File

@ -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
);
});
});
});

View File

@ -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(() => {

View File

@ -8,7 +8,7 @@
}
.label {
width: 90%;
width: 50%;
}
}

View File

@ -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',

View File

@ -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,
}),

View File

@ -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',
}

View File

@ -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;

View File

@ -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: {

View File

@ -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
*/

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "값",

View File

@ -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",

View File

@ -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",

View File

@ -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": "取值",

View File

@ -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": "值",

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 := &regtesting.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 := &regtesting.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) {

View File

@ -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)
}

View File

@ -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

View File

@ -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