1
0
mirror of https://github.com/goharbor/harbor.git synced 2026-10-24 21:59:32 +03:00

add stop sbom scanning API (#20200)

* add stop sbom scanning API

1. [UI] support to stop sbom scanning #20200
2. add type for stop scanning api, make it able to support both vulnerability and sbom.
3. refactor the db query to support multiple extra attributes.

Signed-off-by: wang yan <wangyan@vmware.com>
Signed-off-by: xuelichao <xuel@vmware.com>
Co-authored-by: xuelichao <xuel@vmware.com>
This commit is contained in:
Wang Yan
2024-04-09 16:07:47 +08:00
committed by GitHub
parent be648ea47f
commit 461a5fa50d
46 changed files with 2357 additions and 91 deletions

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

View File

@@ -340,15 +340,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)

View File

@@ -381,7 +381,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"))
}
{
@@ -393,7 +393,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"))
}
{
@@ -403,7 +403,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"))
}
{
@@ -412,7 +412,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"))
}
}

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

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

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

@@ -777,6 +777,7 @@
"ARTIFACTS": "Artefacts",
"SIZE": "Taille",
"VULNERABILITY": "Vulnérabilité",
"SBOM": "SBOM",
"BUILD_HISTORY": "Historique de construction",
"SIGNED": "Signé",
"AUTHOR": "Auteur",
@@ -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é",
@@ -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",

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

@@ -60,7 +60,7 @@ func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopSca
return s.SendError(ctx, err)
}
if err := s.scanCtl.Stop(ctx, curArtifact); err != nil {
if err := s.scanCtl.Stop(ctx, curArtifact, params.ScanType.ScanType); err != nil {
return s.SendError(ctx, err)
}

View File

@@ -16,6 +16,7 @@ package handler
import (
"fmt"
"github.com/goharbor/harbor/src/server/v2.0/models"
"testing"
"github.com/stretchr/testify/suite"
@@ -67,12 +68,12 @@ func (suite *ScanTestSuite) TestStopScan() {
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
url := "/projects/library/repositories/nginx/artifacts/sha256:e4f0474a75c510f40b37b6b7dc2516241ffa8bde5a442bde3d372c9519c84d90/scan/stop"
body := models.ScanType{ScanType: "sbom"}
{
// failed to get artifact by reference
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(&artifact.Artifact{}, fmt.Errorf("failed to get artifact by reference")).Once()
res, err := suite.Post(url, nil)
res, err := suite.PostJSON(url, body)
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
@@ -82,7 +83,7 @@ func (suite *ScanTestSuite) TestStopScan() {
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(nil, nil).Once()
mock.OnAnything(suite.scanCtl, "Stop").Return(fmt.Errorf("nil artifact to stop scan")).Once()
res, err := suite.Post(url, nil)
res, err := suite.PostJSON(url, body)
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
@@ -92,7 +93,7 @@ func (suite *ScanTestSuite) TestStopScan() {
mock.OnAnything(suite.artifactCtl, "GetByReference").Return(&artifact.Artifact{}, nil).Once()
mock.OnAnything(suite.scanCtl, "Stop").Return(nil).Once()
res, err := suite.Post(url, nil)
res, err := suite.PostJSON(url, body)
suite.NoError(err)
suite.Equal(202, res.StatusCode)
}

View File

@@ -191,13 +191,13 @@ func (_m *Controller) ScanAll(ctx context.Context, trigger string, async bool) (
return r0, r1
}
// Stop provides a mock function with given fields: ctx, _a1
func (_m *Controller) Stop(ctx context.Context, _a1 *artifact.Artifact) error {
ret := _m.Called(ctx, _a1)
// Stop provides a mock function with given fields: ctx, _a1, capType
func (_m *Controller) Stop(ctx context.Context, _a1 *artifact.Artifact, capType string) error {
ret := _m.Called(ctx, _a1, capType)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact) error); ok {
r0 = rf(ctx, _a1)
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string) error); ok {
r0 = rf(ctx, _a1, capType)
} else {
r0 = ret.Error(0)
}

View File

@@ -11,7 +11,9 @@ class StopScan(base.Base, object):
def stop_scan_artifact(self, project_name, repo_name, reference, expect_status_code = 202, expect_response_body = None, **kwargs):
try:
data, status_code, _ = self._get_client(**kwargs).stop_scan_artifact_with_http_info(project_name, repo_name, reference)
scanType = v2_swagger_client.ScanType()
scanType.scan_type = "vulnerability"
data, status_code, _ = self._get_client(**kwargs).stop_scan_artifact_with_http_info(project_name, repo_name, reference, scanType)
except ApiException as e:
base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None:

View File

@@ -21,7 +21,7 @@ set +e
docker ps
# run db auth api cases
if [ "$1" = 'DB' ]; then
docker run -i --privileged -v $DIR/../../:/drone -v $DIR/../:/ca -w /drone $E2E_IMAGE robot --exclude proxy_cache -v DOCKER_USER:${DOCKER_USER} -v DOCKER_PWD:${DOCKER_PWD} -v ip:$2 -v ip1: -v http_get_ca:false -v HARBOR_PASSWORD:Harbor12345 /drone/tests/robot-cases/Group1-Nightly/Setup.robot /drone/tests/robot-cases/Group0-BAT/API_DB.robot
docker run -i --privileged -v $DIR/../../:/drone -v $DIR/../:/ca -w /drone $E2E_IMAGE robot --exclude proxy_cache --exclude stop_scan -v DOCKER_USER:${DOCKER_USER} -v DOCKER_PWD:${DOCKER_PWD} -v ip:$2 -v ip1: -v http_get_ca:false -v HARBOR_PASSWORD:Harbor12345 /drone/tests/robot-cases/Group1-Nightly/Setup.robot /drone/tests/robot-cases/Group0-BAT/API_DB.robot
elif [ "$1" = 'PROXY_CACHE' ]; then
docker run -i --privileged -v $DIR/../../:/drone -v $DIR/../:/ca -w /drone $E2E_IMAGE robot --include setup --include proxy_cache -v DOCKER_USER:${DOCKER_USER} -v DOCKER_PWD:${DOCKER_PWD} -v ip:$2 -v ip1: -v http_get_ca:false -v HARBOR_PASSWORD:Harbor12345 /drone/tests/robot-cases/Group1-Nightly/Setup.robot /drone/tests/robot-cases/Group0-BAT/API_DB.robot
elif [ "$1" = 'LDAP' ]; then