Compare commits

...

8 Commits

Author SHA1 Message Date
Jirka Vrba 6a176306d1
Merge 901500fef0 into dee73a44f3 2024-04-26 15:15:53 +08:00
Lichao Xue dee73a44f3
Fix UI bugs (#20364)
Signed-off-by: xuelichao <xuel@vmware.com>
2024-04-26 06:56:23 +00:00
Shengwen YU c791b39a26
fix: add stop_scan_payload when call stop scan api (#20353)
Signed-off-by: Shengwen Yu <yshengwen@vmware.com>
2024-04-26 06:13:00 +00:00
Shengwen YU 822784aac8
fix: update to "clr-dg-cell[10]" to fix the pull time tc due to addin… (#20361)
fix: update to "clr-dg-cell[10]" to fix the pull time tc due to adding an SBOM column

Signed-off-by: Shengwen Yu <yshengwen@vmware.com>
2024-04-26 04:28:22 +00:00
Shengwen YU d0cb200ed5
fix: update nightly test case for verifying audit log of image digest (#20354)
Signed-off-by: Shengwen Yu <yshengwen@vmware.com>
2024-04-26 03:44:00 +00:00
Shengwen YU 0e8dce72be
fix: fresh scanner list when updating scanner (#20366)
Signed-off-by: Shengwen Yu <yshengwen@vmware.com>
2024-04-26 10:52:11 +08:00
stonezdj(Daojun Zhang) ec8d692fe6
Add scanner info and report_id to sbom_overview on listing artifact (#20358)
Add scan_status and report_id when scan has a failed task

Signed-off-by: stonezdj <stone.zhang@broadcom.com>
2024-04-25 17:00:35 +08:00
Jirka Vrba 901500fef0 Add support for dockerhub rate-limiting
Signed-off-by: Jiri Vrba <jiri.vrba@firma.seznam.cz>
2024-04-08 11:45:05 +02:00
34 changed files with 248 additions and 120 deletions

View File

@ -6816,6 +6816,8 @@ definitions:
format: int64 format: int64
description: 'Time in seconds required to create the report' description: 'Time in seconds required to create the report'
example: 300 example: 300
scanner:
$ref: '#/definitions/Scanner'
NativeReportSummary: NativeReportSummary:
type: object type: object
description: 'The summary for the native report' description: 'The summary for the native report'

View File

@ -751,6 +751,11 @@ func (bc *basicController) GetSBOMSummary(ctx context.Context, art *ar.Artifact,
reportContent := reports[0].Report reportContent := reports[0].Report
result := map[string]interface{}{} result := map[string]interface{}{}
if len(reportContent) == 0 { if len(reportContent) == 0 {
status := bc.retrieveStatusFromTask(ctx, reports[0].UUID)
if len(status) > 0 {
result[sbomModel.ReportID] = reports[0].UUID
result[sbomModel.ScanStatus] = status
}
log.Debug("no content for current report") log.Debug("no content for current report")
return result, nil return result, nil
} }
@ -758,6 +763,22 @@ func (bc *basicController) GetSBOMSummary(ctx context.Context, art *ar.Artifact,
return result, err return result, err
} }
// retrieve the status from task
func (bc *basicController) retrieveStatusFromTask(ctx context.Context, reportID string) string {
if len(reportID) == 0 {
return ""
}
tasks, err := bc.taskMgr.ListScanTasksByReportUUID(ctx, reportID)
if err != nil {
log.Warningf("can not find the task with report UUID %v, error %v", reportID, err)
return ""
}
if len(tasks) > 0 {
return tasks[0].Status
}
return ""
}
// GetScanLog ... // GetScanLog ...
func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact, uuid string) ([]byte, error) { func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact, uuid string) ([]byte, error) {
if len(uuid) == 0 { if len(uuid) == 0 {

View File

@ -70,9 +70,10 @@ type ControllerTestSuite struct {
tagCtl *tagtesting.FakeController tagCtl *tagtesting.FakeController
registration *scanner.Registration registration *scanner.Registration
artifact *artifact.Artifact artifact *artifact.Artifact
rawReport string wrongArtifact *artifact.Artifact
rawReport string
execMgr *tasktesting.ExecutionManager execMgr *tasktesting.ExecutionManager
taskMgr *tasktesting.Manager taskMgr *tasktesting.Manager
@ -101,6 +102,9 @@ func (suite *ControllerTestSuite) SetupSuite() {
suite.artifact.Digest = "digest-code" suite.artifact.Digest = "digest-code"
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
suite.wrongArtifact = &artifact.Artifact{Artifact: art.Artifact{ID: 2, ProjectID: 1}}
suite.wrongArtifact.Digest = "digest-wrong"
m := &v1.ScannerAdapterMetadata{ m := &v1.ScannerAdapterMetadata{
Scanner: &v1.Scanner{ Scanner: &v1.Scanner{
Name: "Trivy", Name: "Trivy",
@ -202,8 +206,11 @@ func (suite *ControllerTestSuite) SetupSuite() {
Report: `{"sbom_digest": "sha256:1234567890", "scan_status": "Success", "duration": 3, "start_time": "2021-09-01T00:00:00Z", "end_time": "2021-09-01T00:00:03Z"}`, Report: `{"sbom_digest": "sha256:1234567890", "scan_status": "Success", "duration": 3, "start_time": "2021-09-01T00:00:00Z", "end_time": "2021-09-01T00:00:03Z"}`,
}, },
} }
emptySBOMReport := []*scan.Report{{Report: ``, UUID: "rp-uuid-004"}}
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.MimeTypeNativeReport}).Return(reports, nil)
mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(sbomReport, nil) mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(sbomReport, nil)
mgr.On("GetBy", mock.Anything, suite.wrongArtifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(emptySBOMReport, nil)
mgr.On("Get", mock.Anything, "rp-uuid-001").Return(reports[0], 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("UpdateReportData", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil)
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil) mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
@ -654,6 +661,12 @@ func (suite *ControllerTestSuite) TestGenerateSBOMSummary() {
suite.NotNil(dgst) suite.NotNil(dgst)
suite.Equal("Success", status) suite.Equal("Success", status)
suite.Equal("sha256:1234567890", dgst) suite.Equal("sha256:1234567890", dgst)
tasks := []*task.Task{{Status: "Error"}}
suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once()
sum2, err := suite.c.GetSummary(context.TODO(), suite.wrongArtifact, []string{v1.MimeTypeSBOMReport})
suite.Nil(err)
suite.NotNil(sum2)
} }
func TestIsSBOMMimeTypes(t *testing.T) { func TestIsSBOMMimeTypes(t *testing.T) {
@ -683,5 +696,11 @@ func (suite *ControllerTestSuite) TestDeleteArtifactAccessories() {
} }
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
suite.NoError(suite.c.deleteArtifactAccessories(ctx, reports)) suite.NoError(suite.c.deleteArtifactAccessories(ctx, reports))
}
func (suite *ControllerTestSuite) TestRetrieveStatusFromTask() {
tasks := []*task.Task{{Status: "Error"}}
suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once()
status := suite.c.retrieveStatusFromTask(nil, "rp-uuid-004")
suite.Equal("Error", status)
} }

View File

@ -21,7 +21,9 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
@ -124,6 +126,55 @@ func getAdapterInfo() *model.AdapterPattern {
return info return info
} }
// Rate-limit aware wrapper function for client.Do()
// - Avoids being hit by limit by pausing requests when less than 'lowMark' requests remaining.
// - Pauses for given time when limit is hit.
// - Allows 2 more attempts before giving up.
// Reason: Observed (02/2024) penalty for hitting the limit is 120s, normal reset is 60s,
// so it is better to not hit the wall.
func (a *adapter) limitAwareDo(method string, path string, body io.Reader) (*http.Response, error) {
const lowMark = 8
var attemptsLeft = 3
for attemptsLeft > 0 {
clientResp, clientErr := a.client.Do(method, path, body)
if clientErr != nil {
return clientResp, clientErr
}
if clientResp.StatusCode != http.StatusTooManyRequests {
reqsLeft, err := strconv.ParseInt(clientResp.Header.Get("x-ratelimit-remaining"), 10, 64)
if err != nil {
return clientResp, clientErr
}
if reqsLeft < lowMark {
resetTSC, err := strconv.ParseInt(clientResp.Header.Get("x-ratelimit-reset"), 10, 64)
if err == nil {
dur := time.Until(time.Unix(resetTSC, 0))
log.Infof("Rate-limit exhaustion eminent, sleeping for %.1f seconds", dur.Seconds())
time.Sleep(dur)
log.Info("Sleep finished, resuming operation")
}
}
return clientResp, clientErr
}
var dur = time.Duration(0)
seconds, err := strconv.ParseInt(clientResp.Header.Get("retry-after"), 10, 64)
if err != nil {
expireTime, err := http.ParseTime(clientResp.Header.Get("retry-after"))
if err != nil {
return nil, errors.New("blocked by dockerhub rate-limit and missing retry-after header")
}
dur = time.Until(expireTime)
} else {
dur = time.Duration(seconds) * time.Second
}
log.Infof("Rate-limit exhausted, sleeping for %.1f seconds", dur.Seconds())
time.Sleep(dur)
log.Info("Sleep finished, resuming operation")
attemptsLeft--
}
return nil, errors.New("unable to get past dockerhub rate-limit")
}
// PrepareForPush does the prepare work that needed for pushing/uploading the resource // PrepareForPush does the prepare work that needed for pushing/uploading the resource
// eg: create the namespace or repository // eg: create the namespace or repository
func (a *adapter) PrepareForPush(resources []*model.Resource) error { func (a *adapter) PrepareForPush(resources []*model.Resource) error {
@ -159,7 +210,7 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
} }
func (a *adapter) listNamespaces() ([]string, error) { func (a *adapter) listNamespaces() ([]string, error) {
resp, err := a.client.Do(http.MethodGet, listNamespacePath, nil) resp, err := a.limitAwareDo(http.MethodGet, listNamespacePath, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -207,7 +258,7 @@ func (a *adapter) CreateNamespace(namespace *model.Namespace) error {
return err return err
} }
resp, err := a.client.Do(http.MethodPost, createNamespacePath, bytes.NewReader(b)) resp, err := a.limitAwareDo(http.MethodPost, createNamespacePath, bytes.NewReader(b))
if err != nil { if err != nil {
return err return err
} }
@ -228,7 +279,7 @@ func (a *adapter) CreateNamespace(namespace *model.Namespace) error {
// getNamespace get namespace from DockerHub, if the namespace not found, two nil would be returned. // getNamespace get namespace from DockerHub, if the namespace not found, two nil would be returned.
func (a *adapter) getNamespace(namespace string) (*model.Namespace, error) { func (a *adapter) getNamespace(namespace string) (*model.Namespace, error) {
resp, err := a.client.Do(http.MethodGet, getNamespacePath(namespace), nil) resp, err := a.limitAwareDo(http.MethodGet, getNamespacePath(namespace), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -389,7 +440,7 @@ func (a *adapter) DeleteManifest(repository, reference string) error {
return fmt.Errorf("dockerhub only support repo in format <namespace>/<name>, but got: %s", repository) return fmt.Errorf("dockerhub only support repo in format <namespace>/<name>, but got: %s", repository)
} }
resp, err := a.client.Do(http.MethodDelete, deleteTagPath(parts[0], parts[1], reference), nil) resp, err := a.limitAwareDo(http.MethodDelete, deleteTagPath(parts[0], parts[1], reference), nil)
if err != nil { if err != nil {
return err return err
} }
@ -410,7 +461,7 @@ func (a *adapter) DeleteManifest(repository, reference string) error {
// getRepos gets a page of repos from DockerHub // getRepos gets a page of repos from DockerHub
func (a *adapter) getRepos(namespace, name string, page, pageSize int) (*ReposResp, error) { func (a *adapter) getRepos(namespace, name string, page, pageSize int) (*ReposResp, error) {
resp, err := a.client.Do(http.MethodGet, listReposPath(namespace, name, page, pageSize), nil) resp, err := a.limitAwareDo(http.MethodGet, listReposPath(namespace, name, page, pageSize), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -437,7 +488,7 @@ func (a *adapter) getRepos(namespace, name string, page, pageSize int) (*ReposRe
// getTags gets a page of tags for a repo from DockerHub // getTags gets a page of tags for a repo from DockerHub
func (a *adapter) getTags(namespace, repo string, page, pageSize int) (*TagsResp, error) { func (a *adapter) getTags(namespace, repo string, page, pageSize int) (*TagsResp, error) {
resp, err := a.client.Do(http.MethodGet, listTagsPath(namespace, repo, page, pageSize), nil) resp, err := a.limitAwareDo(http.MethodGet, listTagsPath(namespace, repo, page, pageSize), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -27,6 +27,10 @@ const (
Duration = "duration" Duration = "duration"
// ScanStatus ... // ScanStatus ...
ScanStatus = "scan_status" ScanStatus = "scan_status"
// ReportID ...
ReportID = "report_id"
// Scanner ...
Scanner = "scanner"
) )
// Summary includes the sbom summary information // Summary includes the sbom summary information

View File

@ -87,7 +87,7 @@ func (v *scanHandler) RequiredPermissions() []*types.Policy {
// PostScan defines task specific operations after the scan is complete // PostScan defines task specific operations after the scan is complete
func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) { func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) {
sbomContent, err := retrieveSBOMContent(rawReport) sbomContent, s, err := retrieveSBOMContent(rawReport)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -107,7 +107,7 @@ func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel
myLogger.Errorf("error when create accessory from image %v", err) myLogger.Errorf("error when create accessory from image %v", err)
return "", err return "", err
} }
return v.generateReport(startTime, sr.Artifact.Repository, dgst, "Success") return v.generateReport(startTime, sr.Artifact.Repository, dgst, "Success", s)
} }
// annotations defines the annotations for the accessory artifact // annotations defines the annotations for the accessory artifact
@ -121,7 +121,7 @@ func (v *scanHandler) annotations() map[string]string {
} }
} }
func (v *scanHandler) generateReport(startTime time.Time, repository, digest, status string) (string, error) { func (v *scanHandler) generateReport(startTime time.Time, repository, digest, status string, scanner *v1.Scanner) (string, error) {
summary := sbom.Summary{} summary := sbom.Summary{}
endTime := time.Now() endTime := time.Now()
summary[sbom.StartTime] = startTime summary[sbom.StartTime] = startTime
@ -130,6 +130,7 @@ func (v *scanHandler) generateReport(startTime time.Time, repository, digest, st
summary[sbom.SBOMRepository] = repository summary[sbom.SBOMRepository] = repository
summary[sbom.SBOMDigest] = digest summary[sbom.SBOMDigest] = digest
summary[sbom.ScanStatus] = status summary[sbom.ScanStatus] = status
summary[sbom.Scanner] = scanner
rep, err := json.Marshal(summary) rep, err := json.Marshal(summary)
if err != nil { if err != nil {
return "", err return "", err
@ -150,15 +151,15 @@ func registryFQDN(ctx context.Context) string {
} }
// retrieveSBOMContent retrieves the "sbom" field from the raw report // retrieveSBOMContent retrieves the "sbom" field from the raw report
func retrieveSBOMContent(rawReport string) ([]byte, error) { func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) {
rpt := vuln.Report{} rpt := vuln.Report{}
err := json.Unmarshal([]byte(rawReport), &rpt) err := json.Unmarshal([]byte(rawReport), &rpt)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
sbomContent, err := json.Marshal(rpt.SBOM) sbomContent, err := json.Marshal(rpt.SBOM)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return sbomContent, nil return sbomContent, rpt.Scanner, nil
} }

View File

@ -78,6 +78,7 @@ export const ACTION_RESOURCE_I18N_MAP = {
log: 'ROBOT_ACCOUNT.LOG', log: 'ROBOT_ACCOUNT.LOG',
'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY', 'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY',
quota: 'ROBOT_ACCOUNT.QUOTA', quota: 'ROBOT_ACCOUNT.QUOTA',
sbom: 'ROBOT_ACCOUNT.SBOM',
}; };
export function convertKey(key: string) { export function convertKey(key: string) {

View File

@ -13,10 +13,13 @@
[clrIfActive]="currentTabLinkId === 'vulnerability'"> [clrIfActive]="currentTabLinkId === 'vulnerability'">
<clr-tab-content id="vulnerability-content"> <clr-tab-content id="vulnerability-content">
<hbr-artifact-vulnerabilities <hbr-artifact-vulnerabilities
*ngIf="currentTabLinkId === 'vulnerability'"
[artifact]="artifact" [artifact]="artifact"
[projectName]="projectName" [projectName]="projectName"
[projectId]="projectId" [projectId]="projectId"
[repoName]="repoName" [repoName]="repoName"
[scanBtnState]="getScanBtnState()"
[hasEnabledScanner]="hasEnabledScanner()"
[digest]="digest" [digest]="digest"
[vulnerabilitiesLink]=" [vulnerabilitiesLink]="
getVulnerability() getVulnerability()
@ -24,16 +27,18 @@
</clr-tab-content> </clr-tab-content>
</ng-template> </ng-template>
</clr-tab> </clr-tab>
<clr-tab *ngIf="getSbom()"> <clr-tab *ngIf="hasScannerSupportSBOM()">
<button clrTabLink id="sbom" (click)="actionTab('sbom')"> <button clrTabLink id="sbom" (click)="actionTab('sbom')">
{{ 'REPOSITORY.SBOM' | translate }} {{ 'REPOSITORY.SBOM' | translate }}
</button> </button>
<ng-template [clrIfActive]="currentTabLinkId === 'sbom'"> <ng-template [clrIfActive]="currentTabLinkId === 'sbom'">
<clr-tab-content id="sbom-content"> <clr-tab-content id="sbom-content">
<hbr-artifact-sbom <hbr-artifact-sbom
*ngIf="currentTabLinkId === 'sbom'"
[artifact]="artifact" [artifact]="artifact"
[projectName]="projectName" [projectName]="projectName"
[projectId]="projectId" [projectId]="projectId"
[hasScannerSupportSBOM]="hasScannerSupportSBOM()"
[repoName]="repoName" [repoName]="repoName"
[sbomDigest]="sbomDigest"></hbr-artifact-sbom> [sbomDigest]="sbomDigest"></hbr-artifact-sbom>
</clr-tab-content> </clr-tab-content>
@ -50,6 +55,7 @@
[clrIfActive]="currentTabLinkId === 'build-history'"> [clrIfActive]="currentTabLinkId === 'build-history'">
<clr-tab-content> <clr-tab-content>
<hbr-artifact-build-history <hbr-artifact-build-history
*ngIf="currentTabLinkId === 'build-history'"
[buildHistoryLink]=" [buildHistoryLink]="
getBuildHistory() getBuildHistory()
"></hbr-artifact-build-history> "></hbr-artifact-build-history>
@ -67,6 +73,7 @@
[clrIfActive]="currentTabLinkId === 'summary-link'"> [clrIfActive]="currentTabLinkId === 'summary-link'">
<clr-tab-content id="summary-content"> <clr-tab-content id="summary-content">
<hbr-artifact-summary <hbr-artifact-summary
*ngIf="currentTabLinkId === 'summary-link'"
[summaryLink]="getSummary()"></hbr-artifact-summary> [summaryLink]="getSummary()"></hbr-artifact-summary>
</clr-tab-content> </clr-tab-content>
</ng-template> </ng-template>
@ -81,6 +88,7 @@
<ng-template [clrIfActive]="currentTabLinkId === 'depend-link'"> <ng-template [clrIfActive]="currentTabLinkId === 'depend-link'">
<clr-tab-content id="depend-content"> <clr-tab-content id="depend-content">
<hbr-artifact-dependencies <hbr-artifact-dependencies
*ngIf="currentTabLinkId === 'depend-link'"
[dependenciesLink]=" [dependenciesLink]="
getDependencies() getDependencies()
"></hbr-artifact-dependencies> "></hbr-artifact-dependencies>
@ -97,6 +105,7 @@
<ng-template [clrIfActive]="currentTabLinkId === 'value-link'"> <ng-template [clrIfActive]="currentTabLinkId === 'value-link'">
<clr-tab-content id="value-content"> <clr-tab-content id="value-content">
<hbr-artifact-values <hbr-artifact-values
*ngIf="currentTabLinkId === 'value-link'"
[valuesLink]="getValues()"></hbr-artifact-values> [valuesLink]="getValues()"></hbr-artifact-values>
</clr-tab-content> </clr-tab-content>
</ng-template> </ng-template>

View File

@ -4,6 +4,8 @@ import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/additi
import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils'; import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils';
import { SharedTestingModule } from '../../../../../shared/shared.module'; import { SharedTestingModule } from '../../../../../shared/shared.module';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ArtifactListPageService } from '../artifact-list-page/artifact-list-page.service';
import { ClrLoadingState } from '@clr/angular';
describe('ArtifactAdditionsComponent', () => { describe('ArtifactAdditionsComponent', () => {
const mockedAdditionLinks: AdditionLinks = { const mockedAdditionLinks: AdditionLinks = {
@ -12,6 +14,18 @@ describe('ArtifactAdditionsComponent', () => {
href: CURRENT_BASE_HREF + '/test', href: CURRENT_BASE_HREF + '/test',
}, },
}; };
const mockedArtifactListPageService = {
hasScannerSupportSBOM(): boolean {
return true;
},
hasEnabledScanner(): boolean {
return true;
},
getScanBtnState(): ClrLoadingState {
return ClrLoadingState.SUCCESS;
},
init() {},
};
let component: ArtifactAdditionsComponent; let component: ArtifactAdditionsComponent;
let fixture: ComponentFixture<ArtifactAdditionsComponent>; let fixture: ComponentFixture<ArtifactAdditionsComponent>;
@ -20,6 +34,12 @@ describe('ArtifactAdditionsComponent', () => {
imports: [SharedTestingModule], imports: [SharedTestingModule],
declarations: [ArtifactAdditionsComponent], declarations: [ArtifactAdditionsComponent],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
providers: [
{
provide: ArtifactListPageService,
useValue: mockedArtifactListPageService,
},
],
}).compileComponents(); }).compileComponents();
}); });
@ -27,6 +47,7 @@ describe('ArtifactAdditionsComponent', () => {
fixture = TestBed.createComponent(ArtifactAdditionsComponent); fixture = TestBed.createComponent(ArtifactAdditionsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.additionLinks = mockedAdditionLinks; component.additionLinks = mockedAdditionLinks;
component.tab = 'vulnerability';
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -10,7 +10,8 @@ import { ADDITIONS } from './models';
import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/addition-links'; import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/addition-links';
import { AdditionLink } from '../../../../../../../ng-swagger-gen/models/addition-link'; import { AdditionLink } from '../../../../../../../ng-swagger-gen/models/addition-link';
import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact'; import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact';
import { ClrTabs } from '@clr/angular'; import { ClrLoadingState, ClrTabs } from '@clr/angular';
import { ArtifactListPageService } from '../artifact-list-page/artifact-list-page.service';
@Component({ @Component({
selector: 'artifact-additions', selector: 'artifact-additions',
@ -32,14 +33,21 @@ export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit {
@Input() @Input()
tab: string; tab: string;
@Input() currentTabLinkId: string = 'vulnerability'; @Input() currentTabLinkId: string = '';
activeTab: string = null; activeTab: string = null;
@ViewChild('additionsTab') tabs: ClrTabs; @ViewChild('additionsTab') tabs: ClrTabs;
constructor(private ref: ChangeDetectorRef) {} constructor(
private ref: ChangeDetectorRef,
private artifactListPageService: ArtifactListPageService
) {}
ngOnInit(): void { ngOnInit(): void {
this.activeTab = this.tab; this.activeTab = this.tab;
if (!this.activeTab) {
this.currentTabLinkId = 'vulnerability';
}
this.artifactListPageService.init(this.projectId);
} }
ngAfterViewChecked() { ngAfterViewChecked() {
@ -50,6 +58,10 @@ export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit {
this.ref.detectChanges(); this.ref.detectChanges();
} }
hasScannerSupportSBOM(): boolean {
return this.artifactListPageService.hasScannerSupportSBOM();
}
getVulnerability(): AdditionLink { getVulnerability(): AdditionLink {
if ( if (
this.additionLinks && this.additionLinks &&
@ -59,12 +71,7 @@ export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit {
} }
return null; return null;
} }
getSbom(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.SBOMS]) {
return this.additionLinks[ADDITIONS.SBOMS];
}
return {};
}
getBuildHistory(): AdditionLink { getBuildHistory(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) { if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) {
return this.additionLinks[ADDITIONS.BUILD_HISTORY]; return this.additionLinks[ADDITIONS.BUILD_HISTORY];
@ -93,4 +100,12 @@ export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit {
actionTab(tab: string): void { actionTab(tab: string): void {
this.currentTabLinkId = tab; this.currentTabLinkId = tab;
} }
getScanBtnState(): ClrLoadingState {
return this.artifactListPageService.getScanBtnState();
}
hasEnabledScanner(): boolean {
return this.artifactListPageService.hasEnabledScanner();
}
} }

View File

@ -32,12 +32,18 @@
</div> </div>
</div> </div>
</clr-dg-action-bar> </clr-dg-action-bar>
<clr-dg-column [clrDgField]="'package'" class="package-medium">{{ <clr-dg-column
'SBOM.GRID.COLUMN_PACKAGE' | translate [clrDgSortBy]="'name'"
}}</clr-dg-column> [clrDgField]="'name'"
<clr-dg-column [clrDgField]="'version'" class="version-medium">{{ class="package-medium"
'SBOM.GRID.COLUMN_VERSION' | translate >{{ 'SBOM.GRID.COLUMN_PACKAGE' | translate }}</clr-dg-column
}}</clr-dg-column> >
<clr-dg-column
[clrDgSortBy]="'versionInfo'"
[clrDgField]="'versionInfo'"
class="version-medium"
>{{ 'SBOM.GRID.COLUMN_VERSION' | translate }}</clr-dg-column
>
<clr-dg-column>{{ <clr-dg-column>{{
'SBOM.GRID.COLUMN_LICENSE' | translate 'SBOM.GRID.COLUMN_LICENSE' | translate
}}</clr-dg-column> }}</clr-dg-column>

View File

@ -10,7 +10,6 @@ import {
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UserPermissionService } from '../../../../../../shared/services'; import { UserPermissionService } from '../../../../../../shared/services';
import { AdditionLink } from '../../../../../../../../ng-swagger-gen/models/addition-link';
import { ErrorHandler } from '../../../../../../shared/units/error-handler'; import { ErrorHandler } from '../../../../../../shared/units/error-handler';
import { SessionService } from '../../../../../../shared/services/session.service'; import { SessionService } from '../../../../../../shared/services/session.service';
import { SessionUser } from '../../../../../../shared/entities/session-user'; import { SessionUser } from '../../../../../../shared/entities/session-user';

View File

@ -1,13 +1,6 @@
import { import { Component, Input, OnDestroy, OnInit } from '@angular/core';
AfterViewInit,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { ClrDatagridStateInterface, ClrLoadingState } from '@clr/angular'; import { ClrDatagridStateInterface, ClrLoadingState } from '@clr/angular';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { AdditionLink } from '../../../../../../../../ng-swagger-gen/models/addition-link';
import { import {
ScannerVo, ScannerVo,
UserPermissionService, UserPermissionService,
@ -30,7 +23,6 @@ import {
HarborEvent, HarborEvent,
} from '../../../../../../services/event-service/event.service'; } from '../../../../../../services/event-service/event.service';
import { severityText } from '../../../../../left-side-nav/interrogation-services/vulnerability-database/security-hub.interface'; import { severityText } from '../../../../../left-side-nav/interrogation-services/vulnerability-database/security-hub.interface';
import { AppConfigService } from 'src/app/services/app-config.service';
import { import {
ArtifactSbom, ArtifactSbom,
@ -38,8 +30,7 @@ import {
getArtifactSbom, getArtifactSbom,
} from '../../artifact'; } from '../../artifact';
import { ArtifactService } from 'ng-swagger-gen/services'; import { ArtifactService } from 'ng-swagger-gen/services';
import { ScanTypes } from 'src/app/shared/entities/shared.const'; import { ScanTypes } from '../../../../../../shared/entities/shared.const';
import { ArtifactListPageService } from '../../artifact-list-page/artifact-list-page.service';
@Component({ @Component({
selector: 'hbr-artifact-sbom', selector: 'hbr-artifact-sbom',
@ -56,13 +47,12 @@ export class ArtifactSbomComponent implements OnInit, OnDestroy {
@Input() @Input()
sbomDigest: string; sbomDigest: string;
@Input() artifact: Artifact; @Input() artifact: Artifact;
@Input() hasScannerSupportSBOM: boolean = false;
artifactSbom: ArtifactSbom; artifactSbom: ArtifactSbom;
loading: boolean = false; loading: boolean = false;
hasScannerSupportSBOM: boolean = false;
downloadSbomBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; downloadSbomBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
hasSbomPermission: boolean = false; hasSbomPermission: boolean = false;
hasShowLoading: boolean = false; hasShowLoading: boolean = false;
sub: Subscription; sub: Subscription;
hasViewInitWithDelay: boolean = false; hasViewInitWithDelay: boolean = false;
@ -73,16 +63,13 @@ export class ArtifactSbomComponent implements OnInit, OnDestroy {
readonly severityText = severityText; readonly severityText = severityText;
constructor( constructor(
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private appConfigService: AppConfigService,
private artifactService: ArtifactService, private artifactService: ArtifactService,
private artifactListPageService: ArtifactListPageService,
private userPermissionService: UserPermissionService, private userPermissionService: UserPermissionService,
private eventService: EventService, private eventService: EventService,
private session: SessionService private session: SessionService
) {} ) {}
ngOnInit() { ngOnInit() {
this.artifactListPageService.init(this.projectId);
this.getSbom(); this.getSbom();
this.getSbomPermission(); this.getSbomPermission();
if (!this.sub) { if (!this.sub) {
@ -222,8 +209,6 @@ export class ArtifactSbomComponent implements OnInit, OnDestroy {
} }
canDownloadSbom(): boolean { canDownloadSbom(): boolean {
this.hasScannerSupportSBOM =
this.artifactListPageService.hasScannerSupportSBOM();
return ( return (
this.hasScannerSupportSBOM && this.hasScannerSupportSBOM &&
//this.hasSbomPermission && //this.hasSbomPermission &&
@ -234,7 +219,12 @@ export class ArtifactSbomComponent implements OnInit, OnDestroy {
} }
artifactSbomPackages(): ArtifactSbomPackageItem[] { artifactSbomPackages(): ArtifactSbomPackageItem[] {
return this.artifactSbom?.sbomPackage?.packages ?? []; return (
this.artifactSbom?.sbomPackage?.packages?.filter(
item =>
item?.name || item?.versionInfo || item?.licenseConcluded
) ?? []
);
} }
load(state: ClrDatagridStateInterface) { load(state: ClrDatagridStateInterface) {

View File

@ -50,14 +50,13 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
@Input() @Input()
digest: string; digest: string;
@Input() artifact: Artifact; @Input() artifact: Artifact;
@Input() scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
@Input() hasEnabledScanner: boolean = false;
scan_overview: any; scan_overview: any;
scanner: ScannerVo; scanner: ScannerVo;
projectScanner: ScannerVo;
scanningResults: VulnerabilityItem[] = []; scanningResults: VulnerabilityItem[] = [];
loading: boolean = false; loading: boolean = false;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>; severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>;
cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>; cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>;
hasScanningPermission: boolean = false; hasScanningPermission: boolean = false;
@ -112,7 +111,6 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.getVulnerabilities(); this.getVulnerabilities();
this.getScanningPermission(); this.getScanningPermission();
this.getProjectScanner();
if (!this.sub) { if (!this.sub) {
this.sub = this.eventService.subscribe( this.sub = this.eventService.subscribe(
HarborEvent.UPDATE_VULNERABILITY_INFO, HarborEvent.UPDATE_VULNERABILITY_INFO,
@ -203,30 +201,6 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
); );
} }
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(this.projectId).subscribe(
response => {
if (
response &&
'{}' !== JSON.stringify(response) &&
!response.disabled &&
response.health === 'healthy'
) {
this.scanBtnState = ClrLoadingState.SUCCESS;
this.hasEnabledScanner = true;
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
this.projectScanner = response;
},
error => {
this.scanBtnState = ClrLoadingState.ERROR;
}
);
}
getLevel(v: VulnerabilityItem): number { getLevel(v: VulnerabilityItem): number {
if (v && v.severity && SEVERITY_LEVEL_MAP[v.severity]) { if (v && v.severity && SEVERITY_LEVEL_MAP[v.severity]) {
return SEVERITY_LEVEL_MAP[v.severity]; return SEVERITY_LEVEL_MAP[v.severity];

View File

@ -65,10 +65,6 @@
class="action-dropdown" class="action-dropdown"
clrPosition="bottom-left" clrPosition="bottom-left"
*clrIfOpen> *clrIfOpen>
<div
class="dropdown-divider"
role="separator"
aria-hidden="true"></div>
<button <button
clrDropdownItem clrDropdownItem
id="stop-scan" id="stop-scan"

View File

@ -1099,7 +1099,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
res?.filter( res?.filter(
item => item =>
item.type === AccessoryType.SBOM item.type === AccessoryType.SBOM
)?.[0]?.digest ?? null; )?.[0]?.digest ?? undefined;
} }
}, },
error: err => { error: err => {

View File

@ -76,7 +76,7 @@ export enum AccessoryType {
COSIGN = 'signature.cosign', COSIGN = 'signature.cosign',
NOTATION = 'signature.notation', NOTATION = 'signature.notation',
NYDUS = 'accelerator.nydus', NYDUS = 'accelerator.nydus',
SBOM = 'harbor.sbom', SBOM = 'sbom.harbor',
} }
export enum ArtifactType { export enum ArtifactType {

View File

@ -68,9 +68,7 @@ export class SbomTipHistogramComponent {
} }
get noSbom(): boolean { get noSbom(): boolean {
return ( return this.sbomDigest === undefined || this.sbomDigest === '';
this.sbomSummary.scan_status === SBOM_SCAN_STATUS.NOT_GENERATED_SBOM
);
} }
isThemeLight() { isThemeLight() {

View File

@ -142,7 +142,16 @@ export const errorHandler = function (error: any): string {
} }
// Not a standard error return Basically not used cover unknown error // Not a standard error return Basically not used cover unknown error
try { try {
return JSON.parse(error.error).message; const jsonError = JSON.parse(error.error);
if (jsonError.errors && jsonError.errors instanceof Array) {
return (
jsonError.errors?.map(error => error.message) ?? [
'UNKNOWN_ERROR',
]
).join(',');
} else {
return JSON.parse(error.error).message;
}
} catch (err) {} } catch (err) {}
// Not a standard error return Basically not used cover unknown error // Not a standard error return Basically not used cover unknown error
if (typeof error.error === 'string') { if (typeof error.error === 'string') {

View File

@ -406,6 +406,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1061,7 +1062,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -406,6 +406,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1062,7 +1063,7 @@
"GENERATE": "Generate SBOM ", "GENERATE": "Generate SBOM ",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -407,6 +407,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1060,7 +1061,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -406,6 +406,7 @@
"REPOSITORY": "Dépôt", "REPOSITORY": "Dépôt",
"ARTIFACT": "Artefact", "ARTIFACT": "Artefact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessoire", "ACCESSORY": "Accessoire",
"ARTIFACT_ADDITION": "Artefact Addition", "ARTIFACT_ADDITION": "Artefact Addition",
@ -1060,7 +1061,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -403,6 +403,7 @@
"REPOSITORY": "저장소", "REPOSITORY": "저장소",
"ARTIFACT": "아티팩트", "ARTIFACT": "아티팩트",
"SCAN": "스캔", "SCAN": "스캔",
"SBOM": "SBOM",
"TAG": "태그", "TAG": "태그",
"ACCESSORY": "액세서리", "ACCESSORY": "액세서리",
"ARTIFACT_ADDITION": "아티팩트 추가", "ARTIFACT_ADDITION": "아티팩트 추가",
@ -1059,7 +1060,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -404,6 +404,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1058,7 +1059,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -406,6 +406,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1061,7 +1062,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -405,6 +405,7 @@
"REPOSITORY": "仓库", "REPOSITORY": "仓库",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "扫描", "SCAN": "扫描",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "附件", "ACCESSORY": "附件",
"ARTIFACT_ADDITION": "Artifact 额外信息", "ARTIFACT_ADDITION": "Artifact 额外信息",
@ -1059,7 +1060,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -405,6 +405,7 @@
"REPOSITORY": "Repository", "REPOSITORY": "Repository",
"ARTIFACT": "Artifact", "ARTIFACT": "Artifact",
"SCAN": "Scan", "SCAN": "Scan",
"SBOM": "SBOM",
"TAG": "Tag", "TAG": "Tag",
"ACCESSORY": "Accessory", "ACCESSORY": "Accessory",
"ARTIFACT_ADDITION": "Artifact Addition", "ARTIFACT_ADDITION": "Artifact Addition",
@ -1058,7 +1059,7 @@
"GENERATE": "Generate SBOM", "GENERATE": "Generate SBOM",
"DOWNLOAD": "Download SBOM", "DOWNLOAD": "Download SBOM",
"Details": "SBOM details", "Details": "SBOM details",
"STOP": "Stop SBOM", "STOP": "Stop Generate SBOM",
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
}, },
"VULNERABILITY": { "VULNERABILITY": {

View File

@ -21,16 +21,12 @@ import (
"github.com/goharbor/harbor/src/lib" "github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model"
"github.com/goharbor/harbor/src/server/v2.0/handler/model" "github.com/goharbor/harbor/src/server/v2.0/handler/model"
) )
const ( const (
vulnerabilitiesAddition = "vulnerabilities" vulnerabilitiesAddition = "vulnerabilities"
startTime = "start_time"
endTime = "end_time"
scanStatus = "scan_status"
sbomDigest = "sbom_digest"
duration = "duration"
) )
// NewScanReportAssembler returns vul assembler // NewScanReportAssembler returns vul assembler
@ -92,17 +88,19 @@ func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error {
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeSBOMReport}) overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeSBOMReport})
if err != nil { if err != nil {
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, v1.MimeTypeSBOMReport, err) 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 { }
if len(overview) > 0 {
artifact.SBOMOverView = map[string]interface{}{ artifact.SBOMOverView = map[string]interface{}{
startTime: overview[startTime], sbomModel.StartTime: overview[sbomModel.StartTime],
endTime: overview[endTime], sbomModel.EndTime: overview[sbomModel.EndTime],
scanStatus: overview[scanStatus], sbomModel.ScanStatus: overview[sbomModel.ScanStatus],
sbomDigest: overview[sbomDigest], sbomModel.SBOMDigest: overview[sbomModel.SBOMDigest],
duration: overview[duration], sbomModel.Duration: overview[sbomModel.Duration],
sbomModel.ReportID: overview[sbomModel.ReportID],
sbomModel.Scanner: overview[sbomModel.Scanner],
} }
} }
} }
} }
return nil return nil
} }

View File

@ -89,8 +89,11 @@ copy_artifact = Permission("{}/projects/{}/repositories/target_repo/artifacts?fr
delete_artifact = Permission("{}/projects/{}/repositories/target_repo/artifacts/{}".format(harbor_base_url, project_name, source_artifact_tag), "DELETE", 200) delete_artifact = Permission("{}/projects/{}/repositories/target_repo/artifacts/{}".format(harbor_base_url, project_name, source_artifact_tag), "DELETE", 200)
# 6. Resource scan actions: ['read', 'create', 'stop'] # 6. Resource scan actions: ['read', 'create', 'stop']
stop_scan_payload = {
"scan_type": "vulnerability"
}
create_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202) create_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202)
stop_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/stop".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202) stop_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/stop".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "POST", 202, stop_scan_payload)
read_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/83be44fd-1234-5678-b49f-4b6d6e8f5730/log".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "get", 404) read_scan = Permission("{}/projects/{}/repositories/{}/artifacts/{}/scan/83be44fd-1234-5678-b49f-4b6d6e8f5730/log".format(harbor_base_url, project_name, source_artifact_name, source_artifact_tag), "get", 404)
# 7. Resource tag actions: ['list', 'create', 'delete'] # 7. Resource tag actions: ['list', 'create', 'delete']

View File

@ -51,3 +51,4 @@ ${scanner_password_input} //*[@id='scanner-password']
${scanner_token_input} //*[@id='scanner-token'] ${scanner_token_input} //*[@id='scanner-token']
${scanner_apikey_input} //*[@id='scanner-apiKey'] ${scanner_apikey_input} //*[@id='scanner-apiKey']
${scanner_set_default_btn} //*[@id='set-default'] ${scanner_set_default_btn} //*[@id='set-default']
${scanner_list_refresh_btn} //span[@class='refresh-btn']//clr-icon[@role='none']

View File

@ -76,6 +76,7 @@ Resource Harbor-Pages/Job_Service_Dashboard_Elements.robot
Resource Harbor-Pages/SecurityHub.robot Resource Harbor-Pages/SecurityHub.robot
Resource Harbor-Pages/SecurityHub_Elements.robot Resource Harbor-Pages/SecurityHub_Elements.robot
Resource Harbor-Pages/Verify.robot Resource Harbor-Pages/Verify.robot
Resource Harbor-Pages/Vulnerability_Elements.robot
Resource Docker-Util.robot Resource Docker-Util.robot
Resource CNAB_Util.robot Resource CNAB_Util.robot
Resource Helm-Util.robot Resource Helm-Util.robot

View File

@ -885,13 +885,13 @@ Test Case - Audit Log And Purge
# pull artifact # pull artifact
Docker Pull ${ip}/project${d}/${image}:${tag1} Docker Pull ${ip}/project${d}/${image}:${tag1}
Docker Logout ${ip} Docker Logout ${ip}
Verify Log ${user} project${d}/${image}:${sha256} artifact pull Verify Log ${user} project${d}/${image}@${sha256} artifact pull
Go Into Repo project${d} ${image} Go Into Repo project${d} ${image}
# delete artifact # delete artifact
@{tag_list} Create List ${tag1} @{tag_list} Create List ${tag1}
Multi-delete Artifact @{tag_list} Multi-delete Artifact @{tag_list}
Switch To Logs Switch To Logs
Verify Log ${user} project${d}/${image}:${sha256} artifact delete Verify Log ${user} project${d}/${image}@${sha256} artifact delete
Go Into Project project${d} Go Into Project project${d}
# delete repository # delete repository
Delete Repo project${d} ${image} Delete Repo project${d} ${image}
@ -1151,8 +1151,8 @@ Test Case - Retain Image Last Pull Time
Scan Repo ${tag} Succeed Scan Repo ${tag} Succeed
Sleep 15 Sleep 15
Reload Page Reload Page
Retry Wait Element Visible //clr-dg-row//clr-dg-cell[9] Retry Wait Element Visible //clr-dg-row//clr-dg-cell[10]
${last_pull_time}= Get Text //clr-dg-row//clr-dg-cell[9] ${last_pull_time}= Get Text //clr-dg-row//clr-dg-cell[10]
Should Be Empty ${last_pull_time} Should Be Empty ${last_pull_time}
Switch To Configuration System Setting Switch To Configuration System Setting
Set Up Retain Image Last Pull Time disable Set Up Retain Image Last Pull Time disable
@ -1160,8 +1160,8 @@ Test Case - Retain Image Last Pull Time
Scan Repo ${tag} Succeed Scan Repo ${tag} Succeed
Sleep 15 Sleep 15
Reload Page Reload Page
Retry Wait Element Visible //clr-dg-row//clr-dg-cell[9] Retry Wait Element Visible //clr-dg-row//clr-dg-cell[10]
${last_pull_time}= Get Text //clr-dg-row//clr-dg-cell[9] ${last_pull_time}= Get Text //clr-dg-row//clr-dg-cell[10]
Should Not Be Empty ${last_pull_time} Should Not Be Empty ${last_pull_time}
Close Browser Close Browser

View File

@ -182,7 +182,7 @@ Test Case - External Scanner CRUD
Filter Scanner By Name scanner${d} Filter Scanner By Name scanner${d}
Filter Scanner By Endpoint ${SCANNER_ENDPOINT} Filter Scanner By Endpoint ${SCANNER_ENDPOINT}
Retry Wait Element Count //clr-dg-row 1 Retry Wait Element Count //clr-dg-row 1
Retry Wait Until Page Contains Element //clr-dg-row[.//span[text()='scanner${d}'] and .//clr-dg-cell[text()='${SCANNER_ENDPOINT}'] and .//span[text()='Healthy'] and .//clr-dg-cell[text()='None']] Retry Double Keywords When Error Retry Element Click xpath=${scanner_list_refresh_btn} Retry Wait Until Page Contains Element //clr-dg-row[.//span[text()='scanner${d}'] and .//clr-dg-cell[text()='${SCANNER_ENDPOINT}'] and .//span[text()='Healthy'] and .//clr-dg-cell[text()='None']]
# Delete this scanner # Delete this scanner
Delete Scanner scanner${d} Delete Scanner scanner${d}
Close Browser Close Browser