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:
@@ -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 ]
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -343,6 +343,12 @@ func (e *executionDAO) refreshStatus(ctx context.Context, id int64) (bool, strin
|
||||
return status != execution.Status, status, false, err
|
||||
}
|
||||
|
||||
type jsonbStru struct {
|
||||
keyPrefix string
|
||||
key string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.QuerySeter, error) {
|
||||
qs, err := orm.QuerySetter(ctx, &Execution{}, query)
|
||||
if err != nil {
|
||||
@@ -352,39 +358,32 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
||||
// append the filter for "extra attrs"
|
||||
if query != nil && len(query.Keywords) > 0 {
|
||||
var (
|
||||
key string
|
||||
keyPrefix string
|
||||
value interface{}
|
||||
jsonbStrus []jsonbStru
|
||||
args []interface{}
|
||||
)
|
||||
for key, value = range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") {
|
||||
keyPrefix = "ExtraAttrs."
|
||||
break
|
||||
|
||||
for key, value := range query.Keywords {
|
||||
if strings.HasPrefix(key, "ExtraAttrs.") && key != "ExtraAttrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "ExtraAttrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
if strings.HasPrefix(key, "extra_attrs.") {
|
||||
keyPrefix = "extra_attrs."
|
||||
break
|
||||
if strings.HasPrefix(key, "extra_attrs.") && key != "extra_attrs." {
|
||||
jsonbStrus = append(jsonbStrus, jsonbStru{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: key,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(keyPrefix) == 0 || keyPrefix == key {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
// key = extra_attrs.artifact.digest
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
// key = extra_attrs.a.b.c
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
keys := strings.Split(strings.TrimPrefix(key, keyPrefix), ".")
|
||||
var args []interface{}
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, value)
|
||||
inClause, err := orm.CreateInClause(ctx, buildInClauseSQLForExtraAttrs(keys), args...)
|
||||
idSQL, args := buildInClauseSQLForExtraAttrs(jsonbStrus)
|
||||
inClause, err := orm.CreateInClause(ctx, idSQL, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -395,23 +394,60 @@ func (e *executionDAO) querySetter(ctx context.Context, query *q.Query) (orm.Que
|
||||
}
|
||||
|
||||
// Param keys is strings.Split() after trim "extra_attrs."/"ExtraAttrs." prefix
|
||||
func buildInClauseSQLForExtraAttrs(keys []string) string {
|
||||
switch len(keys) {
|
||||
case 0:
|
||||
// won't fall into this case, as the if condition on "keyPrefix == key"
|
||||
// act as a place holder to ensure "default" is equivalent to "len(keys) >= 2"
|
||||
return ""
|
||||
case 1:
|
||||
return "select id from execution where extra_attrs->>?=?"
|
||||
default:
|
||||
// len(keys) >= 2
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
return fmt.Sprintf("select id from execution where extra_attrs->%s->>?=?", s)
|
||||
// key with keyPrefix supports multi-level query operator on PostgreSQL JSON data
|
||||
// examples:
|
||||
// key = extra_attrs.id,
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->>?=?", args = {id, value}
|
||||
//
|
||||
// key = extra_attrs.artifact.digest
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->>?=?", args = {artifact, id, value}
|
||||
//
|
||||
// key = extra_attrs.a.b.c
|
||||
//
|
||||
// ==> sql = "select id from execution where extra_attrs->?->?->>?=?", args = {a, b, c, value}
|
||||
func buildInClauseSQLForExtraAttrs(jsonbStrus []jsonbStru) (string, []interface{}) {
|
||||
if len(jsonbStrus) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var cond string
|
||||
var args []interface{}
|
||||
sql := "select id from execution where"
|
||||
|
||||
for i, jsonbStr := range jsonbStrus {
|
||||
if jsonbStr.key == "" || jsonbStr.value == "" {
|
||||
return "", nil
|
||||
}
|
||||
keys := strings.Split(strings.TrimPrefix(jsonbStr.key, jsonbStr.keyPrefix), ".")
|
||||
if len(keys) == 1 {
|
||||
if i == 0 {
|
||||
cond += "extra_attrs->>?=?"
|
||||
} else {
|
||||
cond += " and extra_attrs->>?=?"
|
||||
}
|
||||
}
|
||||
if len(keys) >= 2 {
|
||||
elements := make([]string, len(keys)-1)
|
||||
for i := range elements {
|
||||
elements[i] = "?"
|
||||
}
|
||||
s := strings.Join(elements, "->")
|
||||
if i == 0 {
|
||||
cond += fmt.Sprintf("extra_attrs->%s->>?=?", s)
|
||||
} else {
|
||||
cond += fmt.Sprintf(" and extra_attrs->%s->>?=?", s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range keys {
|
||||
args = append(args, item)
|
||||
}
|
||||
args = append(args, jsonbStr.value)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", sql, cond), args
|
||||
}
|
||||
|
||||
func buildExecStatusOutdateKey(id int64, vendor string) string {
|
||||
|
||||
@@ -395,22 +395,36 @@ func TestExecutionDAOSuite(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_buildInClauseSQLForExtraAttrs(t *testing.T) {
|
||||
type args struct {
|
||||
keys []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
args []jsonbStru
|
||||
want string
|
||||
}{
|
||||
{"extra_attrs.", args{[]string{}}, ""},
|
||||
{"extra_attrs.id", args{[]string{"id"}}, "select id from execution where extra_attrs->>?=?"},
|
||||
{"extra_attrs.artifact.digest", args{[]string{"artifact", "digest"}}, "select id from execution where extra_attrs->?->>?=?"},
|
||||
{"extra_attrs.a.b.c", args{[]string{"a", "b", "c"}}, "select id from execution where extra_attrs->?->?->>?=?"},
|
||||
{"extra_attrs.", []jsonbStru{}, ""},
|
||||
{"extra_attrs.", []jsonbStru{{}}, ""},
|
||||
{"extra_attrs.id", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.id",
|
||||
value: "1",
|
||||
}}, "select id from execution where extra_attrs->>?=?"},
|
||||
{"extra_attrs.artifact.digest", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.artifact.digest",
|
||||
value: "sha256:1234",
|
||||
}}, "select id from execution where extra_attrs->?->>?=?"},
|
||||
{"extra_attrs.a.b.c", []jsonbStru{{
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.a.b.c",
|
||||
value: "test_value_1",
|
||||
}, {
|
||||
keyPrefix: "extra_attrs.",
|
||||
key: "extra_attrs.d.e",
|
||||
value: "test_value_2",
|
||||
}}, "select id from execution where extra_attrs->?->?->>?=? and extra_attrs->?->>?=?"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := buildInClauseSQLForExtraAttrs(tt.args.keys); got != tt.want {
|
||||
if got, _ := buildInClauseSQLForExtraAttrs(tt.args); got != tt.want {
|
||||
t.Errorf("buildInClauseSQLForExtraAttrs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,4 +18,5 @@ export enum ADDITIONS {
|
||||
SUMMARY = 'readme.md',
|
||||
VALUES = 'values.yaml',
|
||||
DEPENDENCIES = 'dependencies',
|
||||
SBOMS = 'sboms',
|
||||
}
|
||||
|
||||
@@ -1,12 +1,56 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { ArtifactListPageService } from './artifact-list-page.service';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import {
|
||||
ScanningResultService,
|
||||
UserPermissionService,
|
||||
} from 'src/app/shared/services';
|
||||
import { of } from 'rxjs';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
|
||||
describe('ArtifactListPageService', () => {
|
||||
const FakedScanningResultService = {
|
||||
getProjectScanner: () =>
|
||||
of({
|
||||
access_credential: '',
|
||||
adapter: 'Trivy',
|
||||
auth: '',
|
||||
capabilities: {
|
||||
support_sbom: true,
|
||||
support_vulnerability: true,
|
||||
},
|
||||
create_time: '2024-03-06T09:29:43.789Z',
|
||||
description: 'The Trivy scanner adapter',
|
||||
disabled: false,
|
||||
health: 'healthy',
|
||||
is_default: true,
|
||||
name: 'Trivy',
|
||||
skip_certVerify: false,
|
||||
update_time: '2024-03-06T09:29:43.789Z',
|
||||
url: 'http://trivy-adapter:8080',
|
||||
use_internal_addr: true,
|
||||
uuid: '10c68b62-db9c-11ee-9c72-0242ac130009',
|
||||
vendor: 'Aqua Security',
|
||||
version: 'v0.47.0',
|
||||
}),
|
||||
};
|
||||
const FakedUserPermissionService = {
|
||||
hasProjectPermissions: () => of([true, true, true, true, true]),
|
||||
};
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
providers: [ArtifactListPageService],
|
||||
providers: [
|
||||
ArtifactListPageService,
|
||||
{
|
||||
provide: ScanningResultService,
|
||||
useValue: FakedScanningResultService,
|
||||
},
|
||||
{
|
||||
provide: UserPermissionService,
|
||||
useValue: FakedUserPermissionService,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,4 +60,51 @@ describe('ArtifactListPageService', () => {
|
||||
expect(service).toBeTruthy();
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService Permissions validation ', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.init(3);
|
||||
expect(service.hasSbomPermission()).toBeTruthy();
|
||||
expect(service.hasAddLabelImagePermission()).toBeTruthy();
|
||||
expect(service.hasRetagImagePermission()).toBeTruthy();
|
||||
expect(service.hasDeleteImagePermission()).toBeTruthy();
|
||||
expect(service.hasScanImagePermission()).toBeTruthy();
|
||||
expect(service.hasScannerSupportVulnerability()).toBeTruthy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeTruthy();
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService updateStates', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.init(3);
|
||||
expect(service.hasEnabledScanner()).toBeTruthy();
|
||||
expect(service.getScanBtnState()).toBe(ClrLoadingState.SUCCESS);
|
||||
expect(service.getSbomBtnState()).toBe(ClrLoadingState.SUCCESS);
|
||||
service.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
expect(service.hasEnabledScanner()).toBeFalsy();
|
||||
expect(service.getScanBtnState()).toBe(ClrLoadingState.ERROR);
|
||||
expect(service.getSbomBtnState()).toBe(ClrLoadingState.ERROR);
|
||||
}
|
||||
));
|
||||
it('Test ArtifactListPageService updateCapabilities ', inject(
|
||||
[ArtifactListPageService],
|
||||
(service: ArtifactListPageService) => {
|
||||
service.updateCapabilities({
|
||||
support_vulnerability: true,
|
||||
support_sbom: true,
|
||||
});
|
||||
expect(service.hasScannerSupportVulnerability()).toBeTruthy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeTruthy();
|
||||
service.updateCapabilities({
|
||||
support_vulnerability: false,
|
||||
support_sbom: false,
|
||||
});
|
||||
expect(service.hasScannerSupportVulnerability()).toBeFalsy();
|
||||
expect(service.hasScannerSupportSBOM()).toBeFalsy();
|
||||
}
|
||||
));
|
||||
});
|
||||
|
||||
@@ -10,11 +10,15 @@ import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
@Injectable()
|
||||
export class ArtifactListPageService {
|
||||
private _scanBtnState: ClrLoadingState;
|
||||
private _sbomBtnState: ClrLoadingState;
|
||||
private _hasEnabledScanner: boolean = false;
|
||||
private _hasScannerSupportVulnerability: boolean = false;
|
||||
private _hasScannerSupportSBOM: boolean = false;
|
||||
private _hasAddLabelImagePermission: boolean = false;
|
||||
private _hasRetagImagePermission: boolean = false;
|
||||
private _hasDeleteImagePermission: boolean = false;
|
||||
private _hasScanImagePermission: boolean = false;
|
||||
private _hasSbomPermission: boolean = false;
|
||||
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
@@ -26,6 +30,10 @@ export class ArtifactListPageService {
|
||||
return this._scanBtnState;
|
||||
}
|
||||
|
||||
getSbomBtnState(): ClrLoadingState {
|
||||
return this._sbomBtnState;
|
||||
}
|
||||
|
||||
hasEnabledScanner(): boolean {
|
||||
return this._hasEnabledScanner;
|
||||
}
|
||||
@@ -46,14 +54,53 @@ export class ArtifactListPageService {
|
||||
return this._hasScanImagePermission;
|
||||
}
|
||||
|
||||
hasSbomPermission(): boolean {
|
||||
return this._hasSbomPermission;
|
||||
}
|
||||
|
||||
hasScannerSupportVulnerability(): boolean {
|
||||
return this._hasScannerSupportVulnerability;
|
||||
}
|
||||
|
||||
hasScannerSupportSBOM(): boolean {
|
||||
return this._hasScannerSupportSBOM;
|
||||
}
|
||||
|
||||
init(projectId: number) {
|
||||
this._getProjectScanner(projectId);
|
||||
this._getPermissionRule(projectId);
|
||||
}
|
||||
|
||||
updateStates(
|
||||
enabledScanner: boolean,
|
||||
scanState?: ClrLoadingState,
|
||||
sbomState?: ClrLoadingState
|
||||
) {
|
||||
if (scanState) {
|
||||
this._scanBtnState = scanState;
|
||||
}
|
||||
if (sbomState) {
|
||||
this._sbomBtnState = sbomState;
|
||||
}
|
||||
this._hasEnabledScanner = enabledScanner;
|
||||
}
|
||||
|
||||
updateCapabilities(capabilities?: any) {
|
||||
if (capabilities) {
|
||||
if (capabilities?.support_vulnerability !== undefined) {
|
||||
this._hasScannerSupportVulnerability =
|
||||
capabilities.support_vulnerability;
|
||||
}
|
||||
if (capabilities?.support_sbom !== undefined) {
|
||||
this._hasScannerSupportSBOM = capabilities.support_sbom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getProjectScanner(projectId: number): void {
|
||||
this._hasEnabledScanner = false;
|
||||
this._scanBtnState = ClrLoadingState.LOADING;
|
||||
this._sbomBtnState = ClrLoadingState.LOADING;
|
||||
this.scanningService.getProjectScanner(projectId).subscribe(
|
||||
response => {
|
||||
if (
|
||||
@@ -62,14 +109,28 @@ export class ArtifactListPageService {
|
||||
!response.disabled &&
|
||||
response.health === 'healthy'
|
||||
) {
|
||||
this._scanBtnState = ClrLoadingState.SUCCESS;
|
||||
this._hasEnabledScanner = true;
|
||||
this.updateStates(
|
||||
true,
|
||||
ClrLoadingState.SUCCESS,
|
||||
ClrLoadingState.SUCCESS
|
||||
);
|
||||
if (response?.capabilities) {
|
||||
this.updateCapabilities(response?.capabilities);
|
||||
}
|
||||
} else {
|
||||
this._scanBtnState = ClrLoadingState.ERROR;
|
||||
this.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this._scanBtnState = ClrLoadingState.ERROR;
|
||||
this.updateStates(
|
||||
false,
|
||||
ClrLoadingState.ERROR,
|
||||
ClrLoadingState.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -94,6 +155,11 @@ export class ArtifactListPageService {
|
||||
action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE
|
||||
.CREATE,
|
||||
},
|
||||
{
|
||||
resource: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.KEY,
|
||||
action: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.VALUE
|
||||
.CREATE,
|
||||
},
|
||||
];
|
||||
this.userPermissionService
|
||||
.hasProjectPermissions(projectId, permissions)
|
||||
@@ -103,6 +169,9 @@ export class ArtifactListPageService {
|
||||
this._hasRetagImagePermission = results[1];
|
||||
this._hasDeleteImagePermission = results[2];
|
||||
this._hasScanImagePermission = results[3];
|
||||
this._hasSbomPermission = results?.[4] ?? false;
|
||||
// TODO need to remove the static code
|
||||
this._hasSbomPermission = true;
|
||||
},
|
||||
error => this.errorHandlerService.error(error)
|
||||
);
|
||||
|
||||
@@ -42,6 +42,34 @@
|
||||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'VULNERABILITY.STOP_NOW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasEnabledSbom()"
|
||||
id="generate-sbom-btn"
|
||||
[clrLoading]="generateSbomBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="true"
|
||||
(click)="generateSbom()">
|
||||
<clr-icon shape="file" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
id="stop-sbom-btn"
|
||||
*ngIf="hasEnabledSbom()"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn"
|
||||
[disabled]="
|
||||
!(
|
||||
canStopSbom() &&
|
||||
hasSbomPermission &&
|
||||
hasEnabledScanner
|
||||
)
|
||||
"
|
||||
(click)="stopSbom()">
|
||||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.STOP' | translate }}</span>
|
||||
</button>
|
||||
<clr-dropdown class="btn btn-link" *ngIf="!depth">
|
||||
<span
|
||||
clrDropdownTrigger
|
||||
@@ -174,23 +202,29 @@
|
||||
{{ 'REPOSITORY.VULNERABILITY' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="annotations-column">
|
||||
<clr-dg-column class="sbom-column">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[6] }">
|
||||
{{ 'REPOSITORY.SBOM' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="annotations-column">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[7] }">
|
||||
{{ 'ARTIFACT.ANNOTATION' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[7] }">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[8] }">
|
||||
{{ 'REPOSITORY.LABELS' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pushComparator">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[8] }">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[9] }">
|
||||
{{ 'REPOSITORY.PUSH_TIME' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pullComparator">
|
||||
<ng-template [clrDgHideableColumn]="{ hidden: hiddenArray[9] }">
|
||||
<ng-template
|
||||
[clrDgHideableColumn]="{ hidden: hiddenArray[10] }">
|
||||
{{ 'REPOSITORY.PULL_TIME' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-column>
|
||||
@@ -389,6 +423,26 @@
|
||||
</hbr-vulnerability-bar>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div class="cell">
|
||||
<span *ngIf="!hasScannerSupportSBOM">
|
||||
{{ 'ARTIFACT.SBOM_UNSUPPORTED' | translate }}
|
||||
</span>
|
||||
<hbr-sbom-bar
|
||||
(submitStopFinish)="submitSbomStopFinish($event)"
|
||||
(scanFinished)="sbomFinished($event)"
|
||||
*ngIf="hasScannerSupportSBOM"
|
||||
[inputScanner]="artifact?.sbom_overview?.scanner"
|
||||
(submitFinish)="submitSbomFinish($event)"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[artifactDigest]="artifact?.digest"
|
||||
[sbomDigest]="artifact?.sbomDigest"
|
||||
[sbomOverview]="artifact?.sbom_overview">
|
||||
</hbr-sbom-bar>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div class="cell" *ngIf="artifact.annotationsArray?.length">
|
||||
<div class="bar-state">
|
||||
|
||||
@@ -161,6 +161,10 @@
|
||||
width: 11rem !important;
|
||||
}
|
||||
|
||||
.sbom-column {
|
||||
width: 6rem !important;
|
||||
}
|
||||
|
||||
.annotations-column {
|
||||
width: 5rem !important;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ScanningResultDefaultService,
|
||||
ScanningResultService,
|
||||
} from '../../../../../../../shared/services';
|
||||
import { ArtifactFront as Artifact } from '../../../artifact';
|
||||
import { ArtifactFront as Artifact, ArtifactFront } from '../../../artifact';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
import { OperationService } from '../../../../../../../shared/components/operation/operation.service';
|
||||
import { ArtifactService as NewArtifactService } from '../../../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
@@ -24,6 +24,10 @@ import { ArtifactListPageService } from '../../artifact-list-page.service';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
import { ArtifactModule } from '../../../artifact.module';
|
||||
import {
|
||||
SBOM_SCAN_STATUS,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
} from 'src/app/shared/units/utils';
|
||||
|
||||
describe('ArtifactListTabComponent', () => {
|
||||
let comp: ArtifactListTabComponent;
|
||||
@@ -171,6 +175,16 @@ describe('ArtifactListTabComponent', () => {
|
||||
pull_time: '0001-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
const mockAccessory = <Accessory>{
|
||||
id: 1,
|
||||
artifact_id: 2,
|
||||
subject_artifact_id: 3,
|
||||
subject_artifact_digest: 'fakeDigest',
|
||||
subject_artifact_repo: 'test',
|
||||
size: 120,
|
||||
digest: 'fakeDigest',
|
||||
type: 'test',
|
||||
};
|
||||
const mockErrorHandler = {
|
||||
error: () => {},
|
||||
};
|
||||
@@ -236,9 +250,18 @@ describe('ArtifactListTabComponent', () => {
|
||||
getScanBtnState(): ClrLoadingState {
|
||||
return ClrLoadingState.DEFAULT;
|
||||
},
|
||||
getSbomBtnState(): ClrLoadingState {
|
||||
return ClrLoadingState.DEFAULT;
|
||||
},
|
||||
hasEnabledScanner(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasSbomPermission(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasScannerSupportSBOM(): boolean {
|
||||
return true;
|
||||
},
|
||||
hasAddLabelImagePermission(): boolean {
|
||||
return true;
|
||||
},
|
||||
@@ -353,6 +376,27 @@ describe('ArtifactListTabComponent', () => {
|
||||
fixture.nativeElement.querySelector('.confirmation-title')
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('Generate SBOM button should be disabled', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
const generatedButton =
|
||||
fixture.nativeElement.querySelector('#generate-sbom-btn');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(generatedButton.disabled).toBeTruthy();
|
||||
});
|
||||
it('Stop SBOM button should be disabled', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
const stopButton =
|
||||
fixture.nativeElement.querySelector('#stop-sbom-btn');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(stopButton.disabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('the length of hide array should equal to the number of column', async () => {
|
||||
comp.loading = false;
|
||||
fixture.detectChanges();
|
||||
@@ -360,6 +404,93 @@ describe('ArtifactListTabComponent', () => {
|
||||
const cols = fixture.nativeElement.querySelectorAll('.datagrid-column');
|
||||
expect(cols.length).toEqual(comp.hiddenArray.length);
|
||||
});
|
||||
|
||||
it('Test isEllipsisActive', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(
|
||||
comp.isEllipsisActive(document.createElement('span'))
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
it('Test deleteAccessory', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.deleteAccessory(mockAccessory);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.confirmation-content')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('Test scanNow', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.scanNow();
|
||||
expect(comp.onScanArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test stopNow', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.stopNow();
|
||||
expect(comp.onStopScanArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test stopSbom', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
comp.selectedRow = mockArtifacts.slice(0, 1);
|
||||
comp.stopSbom();
|
||||
expect(comp.onStopSbomArtifactsLength).toBe(1);
|
||||
});
|
||||
it('Test tagsString and isRunningState and canStopSbom and canStopScan', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.tagsString([])).toBeNull();
|
||||
expect(
|
||||
comp.isRunningState(VULNERABILITY_SCAN_STATUS.RUNNING)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
comp.isRunningState(VULNERABILITY_SCAN_STATUS.ERROR)
|
||||
).toBeFalsy();
|
||||
expect(comp.canStopSbom()).toBeFalsy();
|
||||
expect(comp.canStopScan()).toBeFalsy();
|
||||
});
|
||||
it('Test status and handleScanOverview', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.scanStatus(mockArtifacts[0])).toBe(
|
||||
VULNERABILITY_SCAN_STATUS.ERROR
|
||||
);
|
||||
expect(comp.sbomStatus(null)).toBe(SBOM_SCAN_STATUS.NOT_GENERATED_SBOM);
|
||||
expect(comp.sbomStatus(mockArtifacts[0])).toBe(
|
||||
SBOM_SCAN_STATUS.NOT_GENERATED_SBOM
|
||||
);
|
||||
expect(comp.handleScanOverview(mockArtifacts[0])).not.toBeNull();
|
||||
});
|
||||
it('Test utils', async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(comp.selectedRowHasSbom()).toBeFalsy();
|
||||
expect(comp.selectedRowHasVul()).toBeFalsy();
|
||||
expect(comp.canScanNow()).toBeFalsy();
|
||||
expect(comp.hasEnabledSbom()).toBeTruthy();
|
||||
expect(comp.canAddLabel()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
async function stepOpenAction(fixture, comp) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
setHiddenArrayToLocalStorage,
|
||||
setPageSizeToLocalStorage,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
SBOM_SCAN_STATUS,
|
||||
} from '../../../../../../../shared/units/utils';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
import { ArtifactService } from '../../../artifact.service';
|
||||
@@ -76,7 +77,7 @@ import {
|
||||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../../../../../services/event-service/event.service';
|
||||
import { AppConfigService } from 'src/app/services/app-config.service';
|
||||
import { AppConfigService } from '../../../../../../../services/app-config.service';
|
||||
import { ArtifactListPageService } from '../../artifact-list-page.service';
|
||||
import { ACCESSORY_PAGE_SIZE } from './sub-accessories/sub-accessories.component';
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
@@ -141,28 +142,60 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
get hasScanImagePermission(): boolean {
|
||||
return this.artifactListPageService.hasScanImagePermission();
|
||||
}
|
||||
get hasSbomPermission(): boolean {
|
||||
return this.artifactListPageService.hasSbomPermission();
|
||||
}
|
||||
get hasEnabledScanner(): boolean {
|
||||
return this.artifactListPageService.hasEnabledScanner();
|
||||
}
|
||||
get hasScannerSupportVulnerability(): boolean {
|
||||
return this.artifactListPageService.hasScannerSupportVulnerability();
|
||||
}
|
||||
get hasScannerSupportSBOM(): boolean {
|
||||
return this.artifactListPageService.hasScannerSupportSBOM();
|
||||
}
|
||||
get scanBtnState(): ClrLoadingState {
|
||||
return this.artifactListPageService.getScanBtnState();
|
||||
}
|
||||
get generateSbomBtnState(): ClrLoadingState {
|
||||
return this.artifactListPageService.getSbomBtnState();
|
||||
}
|
||||
onSendingScanCommand: boolean;
|
||||
onSendingStopScanCommand: boolean = false;
|
||||
onStopScanArtifactsLength: number = 0;
|
||||
scanStoppedArtifactLength: number = 0;
|
||||
|
||||
onSendingSbomCommand: boolean;
|
||||
onSendingStopSbomCommand: boolean = false;
|
||||
onStopSbomArtifactsLength: number = 0;
|
||||
sbomStoppedArtifactLength: number = 0;
|
||||
|
||||
artifactDigest: string;
|
||||
depth: string;
|
||||
// could Pagination filter
|
||||
filters: string[];
|
||||
scanFinishedArtifactLength: number = 0;
|
||||
onScanArtifactsLength: number = 0;
|
||||
sbomFinishedArtifactLength: number = 0;
|
||||
onSbomArtifactsLength: number = 0;
|
||||
stopBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
updateArtifactSub: Subscription;
|
||||
|
||||
hiddenArray: boolean[] = getHiddenArrayFromLocalStorage(
|
||||
PageSizeMapKeys.ARTIFACT_LIST_TAB_COMPONENT,
|
||||
[false, false, false, false, false, false, true, false, false, false]
|
||||
[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]
|
||||
);
|
||||
deleteAccessorySub: Subscription;
|
||||
copyDigestSub: Subscription;
|
||||
@@ -203,7 +236,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
ngOnInit() {
|
||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||
const appConfig = this.appConfigService.getConfig();
|
||||
this.registryUrl = appConfig.registry_url;
|
||||
this.initRouterData();
|
||||
if (!this.updateArtifactSub) {
|
||||
this.updateArtifactSub = this.eventService.subscribe(
|
||||
@@ -250,7 +284,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
this.copyDigestSub.unsubscribe();
|
||||
this.copyDigestSub = null;
|
||||
}
|
||||
this.datagrid['columnsService']?.columns?.forEach((item, index) => {
|
||||
this.datagrid?.['columnsService']?.columns?.forEach((item, index) => {
|
||||
if (this.depth) {
|
||||
this.hiddenArray[index] = !!item?._value?.hidden;
|
||||
} else {
|
||||
@@ -326,6 +360,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
withImmutableStatus: true,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
withAccessory: false,
|
||||
@@ -350,6 +385,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
withImmutableStatus: true,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
XAcceptVulnerabilities:
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
@@ -381,7 +417,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.getArtifactTagsAsync(this.artifactList);
|
||||
this.getAccessoriesAsync(this.artifactList);
|
||||
this.checkCosignAsync(this.artifactList);
|
||||
this.checkCosignAndSbomAsync(this.artifactList);
|
||||
this.getIconsFromBackEnd();
|
||||
},
|
||||
error => {
|
||||
@@ -420,7 +456,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
this.artifactList = res.body;
|
||||
this.getArtifactTagsAsync(this.artifactList);
|
||||
this.getAccessoriesAsync(this.artifactList);
|
||||
this.checkCosignAsync(this.artifactList);
|
||||
this.checkCosignAndSbomAsync(this.artifactList);
|
||||
this.getIconsFromBackEnd();
|
||||
},
|
||||
error => {
|
||||
@@ -519,6 +555,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
return formatSize(tagSize);
|
||||
}
|
||||
|
||||
hasEnabledSbom(): boolean {
|
||||
return (
|
||||
this.hasScannerSupportSBOM &&
|
||||
this.hasEnabledScanner &&
|
||||
this.hasSbomPermission
|
||||
);
|
||||
}
|
||||
|
||||
retag() {
|
||||
if (this.selectedRow && this.selectedRow.length && !this.depth) {
|
||||
this.copyArtifactComponent.retag(this.selectedRow[0].digest);
|
||||
@@ -714,6 +758,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Get sbom status
|
||||
sbomStatus(artifact: Artifact): string {
|
||||
if (artifact) {
|
||||
let so = (<any>artifact).sbom_overview;
|
||||
if (so && so.scan_status) {
|
||||
return so.scan_status;
|
||||
}
|
||||
}
|
||||
return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM;
|
||||
}
|
||||
|
||||
// Get vulnerability scanning status
|
||||
scanStatus(artifact: Artifact): string {
|
||||
if (artifact) {
|
||||
@@ -771,6 +826,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
selectedRowHasSbom(): boolean {
|
||||
return !!(this.selectedRow && this.selectedRow[0]);
|
||||
}
|
||||
|
||||
hasVul(artifact: Artifact): boolean {
|
||||
return !!(
|
||||
artifact &&
|
||||
@@ -779,6 +838,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
hasSbom(artifact: Artifact): boolean {
|
||||
return !!(
|
||||
artifact &&
|
||||
artifact.addition_links &&
|
||||
artifact.addition_links[ADDITIONS.SBOMS]
|
||||
);
|
||||
}
|
||||
|
||||
submitFinish(e: boolean) {
|
||||
this.scanFinishedArtifactLength += 1;
|
||||
// all selected scan action has started
|
||||
@@ -794,9 +861,27 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
this.onSendingScanCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
submitSbomFinish(e: boolean) {
|
||||
this.sbomFinishedArtifactLength += 1;
|
||||
// all selected scan action has started
|
||||
if (this.sbomFinishedArtifactLength === this.onSbomArtifactsLength) {
|
||||
this.onSendingSbomCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
submitSbomStopFinish(e: boolean) {
|
||||
this.sbomStoppedArtifactLength += 1;
|
||||
// all selected scan action has stopped
|
||||
if (this.sbomStoppedArtifactLength === this.onStopSbomArtifactsLength) {
|
||||
this.onSendingSbomCommand = e;
|
||||
}
|
||||
}
|
||||
|
||||
handleScanOverview(scanOverview: any): any {
|
||||
if (scanOverview) {
|
||||
return Object.values(scanOverview)[0];
|
||||
const keys = Object.keys(scanOverview) ?? [];
|
||||
return keys.length > 0 ? scanOverview[keys[0]] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -857,6 +942,11 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// when finished, remove it from selectedRow
|
||||
sbomFinished(artifact: Artifact) {
|
||||
this.scanFinished(artifact);
|
||||
}
|
||||
|
||||
getIconsFromBackEnd() {
|
||||
if (this.artifactList && this.artifactList.length) {
|
||||
this.artifactService.getIconsFromBackEnd(this.artifactList);
|
||||
@@ -929,11 +1019,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
checkCosignAsync(artifacts: ArtifactFront[]) {
|
||||
|
||||
checkCosignAndSbomAsync(artifacts: ArtifactFront[]) {
|
||||
if (artifacts) {
|
||||
if (artifacts.length) {
|
||||
artifacts.forEach(item => {
|
||||
item.signed = CHECKING;
|
||||
// const sbomOverview = item?.sbom_overview;
|
||||
// item.sbomDigest = sbomOverview?.sbom_digest;
|
||||
// let queryTypes = `${AccessoryType.COSIGN} ${AccessoryType.NOTATION}`;
|
||||
// if (!item.sbomDigest) {
|
||||
// queryTypes = `${queryTypes} ${AccessoryType.SBOM}`;
|
||||
// }
|
||||
this.newArtifactService
|
||||
.listAccessories({
|
||||
projectName: this.projectName,
|
||||
@@ -979,6 +1076,24 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return true if all selected rows are in "running" state
|
||||
canStopSbom(): boolean {
|
||||
if (this.onSendingStopSbomCommand) {
|
||||
return false;
|
||||
}
|
||||
if (this.selectedRow && this.selectedRow.length) {
|
||||
let flag: boolean = true;
|
||||
this.selectedRow.forEach(item => {
|
||||
const st: string = this.sbomStatus(item);
|
||||
if (!this.isRunningState(st)) {
|
||||
flag = false;
|
||||
}
|
||||
});
|
||||
return flag;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isRunningState(state: string): boolean {
|
||||
return (
|
||||
state === VULNERABILITY_SCAN_STATUS.RUNNING ||
|
||||
@@ -1001,6 +1116,22 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stopSbom() {
|
||||
if (this.selectedRow && this.selectedRow.length) {
|
||||
this.sbomStoppedArtifactLength = 0;
|
||||
this.onStopSbomArtifactsLength = this.selectedRow.length;
|
||||
this.onSendingStopSbomCommand = true;
|
||||
this.selectedRow.forEach((data: any) => {
|
||||
let digest = data.digest;
|
||||
this.eventService.publish(
|
||||
HarborEvent.STOP_SBOM_ARTIFACT,
|
||||
this.repoName + '/' + digest
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tagsString(tags: Tag[]): string {
|
||||
if (tags?.length) {
|
||||
const arr: string[] = [];
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ArtifactVulnerabilitiesComponent } from './artifact-additions/artifact-
|
||||
import { ArtifactDefaultService, ArtifactService } from './artifact.service';
|
||||
import { ArtifactDetailRoutingResolverService } from '../../../../services/routing-resolvers/artifact-detail-routing-resolver.service';
|
||||
import { ResultBarChartComponent } from './vulnerability-scanning/result-bar-chart.component';
|
||||
import { ResultSbomComponent } from './sbom-scanning/sbom-scan.component';
|
||||
import { ResultTipHistogramComponent } from './vulnerability-scanning/result-tip-histogram/result-tip-histogram.component';
|
||||
import { HistogramChartComponent } from './vulnerability-scanning/histogram-chart/histogram-chart.component';
|
||||
import { ArtifactInfoComponent } from './artifact-list-page/artifact-list/artifact-info/artifact-info.component';
|
||||
@@ -24,6 +25,7 @@ import { CopyArtifactComponent } from './artifact-list-page/artifact-list/artifa
|
||||
import { CopyDigestComponent } from './artifact-list-page/artifact-list/artifact-list-tab/copy-digest/copy-digest.component';
|
||||
import { ArtifactFilterComponent } from './artifact-list-page/artifact-list/artifact-list-tab/artifact-filter/artifact-filter.component';
|
||||
import { PullCommandComponent } from './artifact-list-page/artifact-list/artifact-list-tab/pull-command/pull-command.component';
|
||||
import { SbomTipHistogramComponent } from './sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -80,6 +82,8 @@ const routes: Routes = [
|
||||
BuildHistoryComponent,
|
||||
ArtifactVulnerabilitiesComponent,
|
||||
ResultBarChartComponent,
|
||||
ResultSbomComponent,
|
||||
SbomTipHistogramComponent,
|
||||
ResultTipHistogramComponent,
|
||||
HistogramChartComponent,
|
||||
ArtifactInfoComponent,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* tslint:disable */
|
||||
|
||||
import { Scanner } from 'ng-swagger-gen/models';
|
||||
|
||||
/**
|
||||
* The generate SBOM overview information
|
||||
*/
|
||||
export interface SBOMOverview {
|
||||
/**
|
||||
* id of the native sbom report
|
||||
*/
|
||||
report_id?: string;
|
||||
|
||||
/**
|
||||
* The start time of the scan process that generating report
|
||||
*/
|
||||
start_time?: string;
|
||||
|
||||
/**
|
||||
* The end time of the scan process that generating report
|
||||
*/
|
||||
end_time?: string;
|
||||
|
||||
/**
|
||||
* The status of the generate SBOM process
|
||||
*/
|
||||
scan_status?: string;
|
||||
|
||||
/**
|
||||
* The digest of the generated SBOM accessory
|
||||
*/
|
||||
sbom_digest?: string;
|
||||
|
||||
/**
|
||||
* The seconds spent for generating the report
|
||||
*/
|
||||
duration?: number;
|
||||
scanner?: Scanner;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="bar-wrapper">
|
||||
<div *ngIf="queued" class="bar-state">
|
||||
<span class="label label-orange">{{
|
||||
'SBOM.STATE.QUEUED' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div *ngIf="error" class="bar-state bar-state-error">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="error-text"
|
||||
target="_blank"
|
||||
[href]="viewLog()">
|
||||
<clr-icon shape="error" class="is-error" size="24"></clr-icon>
|
||||
{{ 'SBOM.STATE.ERROR' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="generating" class="bar-state bar-state-chart">
|
||||
<div>{{ 'SBOM.STATE.SCANNING' | translate }}</div>
|
||||
<div class="progress loop loop-height"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart">
|
||||
<hbr-sbom-tip-histogram
|
||||
[scanner]="getScanner()"
|
||||
[artifactDigest]="artifactDigest"
|
||||
[sbomSummary]="sbomOverview"
|
||||
[sbomDigest]="sbomDigest"></hbr-sbom-tip-histogram>
|
||||
</div>
|
||||
<div *ngIf="stopped" class="bar-state">
|
||||
<span class="label stopped">{{
|
||||
'SBOM.STATE.STOPPED' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
<div *ngIf="otherStatus" class="bar-state">
|
||||
<span class="label not-scan">{{
|
||||
'SBOM.STATE.OTHER_STATUS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,245 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ResultSbomComponent } from './sbom-scan.component';
|
||||
import {
|
||||
ScanningResultDefaultService,
|
||||
ScanningResultService,
|
||||
} from '../../../../../shared/services';
|
||||
import { SBOM_SCAN_STATUS } from '../../../../../shared/units/utils';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { SbomTipHistogramComponent } from './sbom-tip-histogram/sbom-tip-histogram.component';
|
||||
import { SBOMOverview } from './sbom-overview';
|
||||
import { of, timer } from 'rxjs';
|
||||
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
||||
import { Artifact } from 'ng-swagger-gen/models';
|
||||
|
||||
describe('ResultSbomComponent (inline template)', () => {
|
||||
let component: ResultSbomComponent;
|
||||
let fixture: ComponentFixture<ResultSbomComponent>;
|
||||
let mockData: SBOMOverview = {
|
||||
scan_status: SBOM_SCAN_STATUS.SUCCESS,
|
||||
end_time: new Date().toUTCString(),
|
||||
};
|
||||
const mockedSbomDigest =
|
||||
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
||||
const mockedSbomOverview = {
|
||||
report_id: '12345',
|
||||
scan_status: 'Error',
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
};
|
||||
const mockedCloneSbomOverview = {
|
||||
report_id: '12346',
|
||||
scan_status: 'Pending',
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
};
|
||||
const FakedScanService = {
|
||||
scanArtifact: () => of({}),
|
||||
stopScanArtifact: () => of({}),
|
||||
};
|
||||
const FakedArtifactService = {
|
||||
getArtifact: () =>
|
||||
of({
|
||||
accessories: null,
|
||||
addition_links: {
|
||||
build_history: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/build_history',
|
||||
},
|
||||
vulnerabilities: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/vulnerabilities',
|
||||
},
|
||||
},
|
||||
digest: 'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3',
|
||||
extra_attrs: {
|
||||
architecture: 'amd64',
|
||||
author: '',
|
||||
config: {
|
||||
Env: [
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
],
|
||||
WorkingDir: '/',
|
||||
},
|
||||
created: '2024-01-10T10:05:33.2702206Z',
|
||||
os: 'linux',
|
||||
},
|
||||
icon: 'sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06',
|
||||
id: 3,
|
||||
labels: null,
|
||||
manifest_media_type:
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
media_type: 'application/vnd.docker.container.image.v1+json',
|
||||
project_id: 3,
|
||||
pull_time: '2024-04-02T01:50:58.332Z',
|
||||
push_time: '2024-03-06T09:47:08.163Z',
|
||||
references: null,
|
||||
repository_id: 2,
|
||||
sbom_overview: {
|
||||
duration: 2,
|
||||
end_time: '2024-04-02T01:50:59.406Z',
|
||||
sbom_digest:
|
||||
'sha256:8cca43ea666e0e7990c2433e3b185313e6ba303cc7a3124bb767823c79fb74a6',
|
||||
scan_status: 'Success',
|
||||
start_time: '2024-04-02T01:50:57.176Z',
|
||||
},
|
||||
size: 3957,
|
||||
tags: null,
|
||||
type: 'IMAGE',
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [ResultSbomComponent, SbomTipHistogramComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ScanningResultService,
|
||||
useValue: ScanningResultDefaultService,
|
||||
},
|
||||
{
|
||||
provide: ScanService,
|
||||
useValue: FakedScanService,
|
||||
},
|
||||
{
|
||||
provide: ArtifactService,
|
||||
useValue: FakedArtifactService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResultSbomComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.repoName = 'mockRepo';
|
||||
component.artifactDigest = mockedSbomDigest;
|
||||
component.sbomDigest = mockedSbomDigest;
|
||||
component.sbomOverview = mockData;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should show "scan stopped" if status is STOPPED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.STOPPED;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('span');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('SBOM.STATE.STOPPED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show progress if status is SCANNING', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.RUNNING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.progress');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show QUEUED if status is QUEUED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.PENDING;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.bar-state');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('span');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent).toEqual('SBOM.STATE.QUEUED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show summary bar chart if status is COMPLETED', () => {
|
||||
component.sbomOverview.scan_status = SBOM_SCAN_STATUS.SUCCESS;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('a');
|
||||
expect(el).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent getScanner', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.getScanner()).toBeUndefined();
|
||||
component.sbomOverview = mockedSbomOverview;
|
||||
expect(component.getScanner()).toBe(mockedSbomOverview.scanner);
|
||||
component.projectName = 'test';
|
||||
component.repoName = 'ui';
|
||||
component.artifactDigest = 'dg';
|
||||
expect(component.viewLog()).toBe(
|
||||
'/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log'
|
||||
);
|
||||
component.copyValue(mockedCloneSbomOverview);
|
||||
expect(component.sbomOverview.report_id).toBe(
|
||||
mockedCloneSbomOverview.report_id
|
||||
);
|
||||
});
|
||||
it('Test ResultSbomComponent status', () => {
|
||||
component.sbomOverview = mockedSbomOverview;
|
||||
fixture.detectChanges();
|
||||
expect(component.status).toBe(SBOM_SCAN_STATUS.ERROR);
|
||||
expect(component.completed).toBeFalsy();
|
||||
expect(component.queued).toBeFalsy();
|
||||
expect(component.generating).toBeFalsy();
|
||||
expect(component.stopped).toBeFalsy();
|
||||
expect(component.otherStatus).toBeFalsy();
|
||||
expect(component.error).toBeTruthy();
|
||||
});
|
||||
it('Test ResultSbomComponent ngOnDestroy', () => {
|
||||
component.stateCheckTimer = timer(0, 10000).subscribe(() => {});
|
||||
component.ngOnDestroy();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
expect(component.generateSbomSubscription).toBeNull();
|
||||
expect(component.stopSubscription).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent generateSbom', () => {
|
||||
fixture.detectChanges();
|
||||
component.generateSbom();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onSubmitting).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent stopSbom', () => {
|
||||
fixture.detectChanges();
|
||||
component.stopSbom();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onStopping).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultSbomComponent getSbomOverview', () => {
|
||||
fixture.detectChanges();
|
||||
component.getSbomOverview();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.stateCheckTimer).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { Subscription, timer } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import { ScannerVo } from '../../../../../shared/services';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import {
|
||||
clone,
|
||||
CURRENT_BASE_HREF,
|
||||
dbEncodeURIComponent,
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
SBOM_SCAN_STATUS,
|
||||
} from '../../../../../shared/units/utils';
|
||||
import { ArtifactService } from '../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact';
|
||||
import {
|
||||
EventService,
|
||||
HarborEvent,
|
||||
} from '../../../../../services/event-service/event.service';
|
||||
import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service';
|
||||
import { ScanType } from 'ng-swagger-gen/models';
|
||||
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
||||
import { SBOMOverview } from './sbom-overview';
|
||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||
const RETRY_TIMES: number = 3;
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-sbom-bar',
|
||||
templateUrl: './sbom-scan-component.html',
|
||||
styleUrls: ['./scanning.scss'],
|
||||
})
|
||||
export class ResultSbomComponent implements OnInit, OnDestroy {
|
||||
@Input() inputScanner: ScannerVo;
|
||||
@Input() repoName: string = '';
|
||||
@Input() projectName: string = '';
|
||||
@Input() projectId: string = '';
|
||||
@Input() artifactDigest: string = '';
|
||||
@Input() sbomDigest: string = '';
|
||||
@Input() sbomOverview: SBOMOverview;
|
||||
onSubmitting: boolean = false;
|
||||
onStopping: boolean = false;
|
||||
retryCounter: number = 0;
|
||||
stateCheckTimer: Subscription;
|
||||
generateSbomSubscription: Subscription;
|
||||
stopSubscription: Subscription;
|
||||
timerHandler: any;
|
||||
@Output()
|
||||
submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
// if sending stop scan request is finished, emit to farther component
|
||||
@Output()
|
||||
submitStopFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output()
|
||||
scanFinished: EventEmitter<Artifact> = new EventEmitter<Artifact>();
|
||||
|
||||
constructor(
|
||||
private artifactService: ArtifactService,
|
||||
private scanService: ScanService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private eventService: EventService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (
|
||||
(this.status === SBOM_SCAN_STATUS.RUNNING ||
|
||||
this.status === SBOM_SCAN_STATUS.PENDING) &&
|
||||
!this.stateCheckTimer
|
||||
) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(0, STATE_CHECK_INTERVAL).subscribe(
|
||||
() => {
|
||||
this.getSbomOverview();
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!this.generateSbomSubscription) {
|
||||
this.generateSbomSubscription = this.eventService.subscribe(
|
||||
HarborEvent.START_GENERATE_SBOM,
|
||||
(artifactDigest: string) => {
|
||||
let myFullTag: string =
|
||||
this.repoName + '/' + this.artifactDigest;
|
||||
if (myFullTag === artifactDigest) {
|
||||
this.generateSbom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!this.stopSubscription) {
|
||||
this.stopSubscription = this.eventService.subscribe(
|
||||
HarborEvent.STOP_SBOM_ARTIFACT,
|
||||
(artifactDigest: string) => {
|
||||
let myFullTag: string =
|
||||
this.repoName + '/' + this.artifactDigest;
|
||||
if (myFullTag === artifactDigest) {
|
||||
this.stopSbom();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
if (this.generateSbomSubscription) {
|
||||
this.generateSbomSubscription.unsubscribe();
|
||||
this.generateSbomSubscription = null;
|
||||
}
|
||||
if (this.stopSubscription) {
|
||||
this.stopSubscription.unsubscribe();
|
||||
this.stopSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get vulnerability scanning status
|
||||
public get status(): string {
|
||||
if (this.sbomOverview && this.sbomOverview.scan_status) {
|
||||
return this.sbomOverview.scan_status;
|
||||
}
|
||||
return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM;
|
||||
}
|
||||
|
||||
public get completed(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.SUCCESS;
|
||||
}
|
||||
|
||||
public get error(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.ERROR;
|
||||
}
|
||||
|
||||
public get queued(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.PENDING;
|
||||
}
|
||||
|
||||
public get generating(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.RUNNING;
|
||||
}
|
||||
|
||||
public get stopped(): boolean {
|
||||
return this.status === SBOM_SCAN_STATUS.STOPPED;
|
||||
}
|
||||
|
||||
public get otherStatus(): boolean {
|
||||
return !(
|
||||
this.completed ||
|
||||
this.error ||
|
||||
this.queued ||
|
||||
this.generating ||
|
||||
this.stopped
|
||||
);
|
||||
}
|
||||
|
||||
generateSbom(): void {
|
||||
if (this.onSubmitting) {
|
||||
// Avoid duplicated submitting
|
||||
console.error('duplicated submit');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
console.error('bad repository or tag');
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmitting = true;
|
||||
|
||||
this.scanService
|
||||
.scanArtifact({
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
// scanType: <ScanType>{
|
||||
// scan_type: ScanTypes.SBOM,
|
||||
// },
|
||||
})
|
||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||
.subscribe(
|
||||
() => {
|
||||
this.onSubmitting = false;
|
||||
// Forcely change status to queued after successful submitting
|
||||
this.sbomOverview = {
|
||||
scan_status: SBOM_SCAN_STATUS.PENDING,
|
||||
};
|
||||
// Start check status util the job is done
|
||||
if (!this.stateCheckTimer) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(
|
||||
STATE_CHECK_INTERVAL,
|
||||
STATE_CHECK_INTERVAL
|
||||
).subscribe(() => {
|
||||
this.getSbomOverview();
|
||||
});
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.onSubmitting = false;
|
||||
if (error && error.error && error.error.code === 409) {
|
||||
console.error(error.error.message);
|
||||
} else {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSbomOverview(): void {
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
return;
|
||||
}
|
||||
this.artifactService
|
||||
.getArtifact({
|
||||
projectName: this.projectName,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: this.artifactDigest,
|
||||
// withSbomOverview: true,
|
||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
})
|
||||
.subscribe(
|
||||
(artifact: Artifact) => {
|
||||
// To keep the same summary reference, use value copy.
|
||||
// if (artifact.sbom_overview) {
|
||||
// this.copyValue(artifact.sbom_overview);
|
||||
// }
|
||||
if (!this.queued && !this.generating) {
|
||||
// Scanning should be done
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
this.scanFinished.emit(artifact);
|
||||
}
|
||||
this.eventService.publish(
|
||||
HarborEvent.UPDATE_SBOM_INFO,
|
||||
artifact
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
this.retryCounter++;
|
||||
if (this.retryCounter >= RETRY_TIMES) {
|
||||
// Stop timer
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
this.stateCheckTimer = null;
|
||||
}
|
||||
this.retryCounter = 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
copyValue(newVal: SBOMOverview): void {
|
||||
if (!this.sbomOverview || !newVal || !newVal.scan_status) {
|
||||
return;
|
||||
}
|
||||
this.sbomOverview = clone(newVal);
|
||||
}
|
||||
|
||||
viewLog(): string {
|
||||
return `${CURRENT_BASE_HREF}/projects/${
|
||||
this.projectName
|
||||
}/repositories/${dbEncodeURIComponent(this.repoName)}/artifacts/${
|
||||
this.artifactDigest
|
||||
}/scan/${this.sbomOverview.report_id}/log`;
|
||||
}
|
||||
|
||||
getScanner(): ScannerVo {
|
||||
if (this.sbomOverview && this.sbomOverview.scanner) {
|
||||
return this.sbomOverview.scanner;
|
||||
}
|
||||
return this.inputScanner;
|
||||
}
|
||||
|
||||
stopSbom() {
|
||||
if (this.onStopping) {
|
||||
// Avoid duplicated stopping command
|
||||
console.error('duplicated stopping command for SBOM generation');
|
||||
return;
|
||||
}
|
||||
if (!this.repoName || !this.artifactDigest) {
|
||||
console.error('bad repository or artifact');
|
||||
return;
|
||||
}
|
||||
this.onStopping = true;
|
||||
|
||||
this.scanService
|
||||
.stopScanArtifact({
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
scanType: <ScanType>{
|
||||
scan_type: ScanTypes.SBOM,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.submitStopFinish.emit(false);
|
||||
this.onStopping = false;
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
() => {
|
||||
// Start check status util the job is done
|
||||
if (!this.stateCheckTimer) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(
|
||||
STATE_CHECK_INTERVAL,
|
||||
STATE_CHECK_INTERVAL
|
||||
).subscribe(() => {
|
||||
this.getSbomOverview();
|
||||
});
|
||||
}
|
||||
this.errorHandler.info('SBOM.TRIGGER_STOP_SUCCESS');
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="tip-wrapper width-215">
|
||||
<clr-tooltip>
|
||||
<div clrTooltipTrigger class="tip-block">
|
||||
<div *ngIf="!noSbom" class="circle-block">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="digest margin-left-5"
|
||||
(click)="goIntoArtifactSbomSummaryPage()"
|
||||
title="{{ 'SBOM.Details' | translate }">
|
||||
{{ 'SBOM.Details' | translate }}</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="noSbom"
|
||||
class="pl-1 margin-left-5 tip-wrapper bar-block-none shadow-none width-150">
|
||||
{{ 'SBOM.NO_SBOM' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<clr-tooltip-content
|
||||
class="w-800"
|
||||
[clrPosition]="'right'"
|
||||
[clrSize]="'lg'"
|
||||
*clrIfOpen>
|
||||
<div class="bar-tooltip-font-larger">
|
||||
<div
|
||||
*ngIf="!noSbom"
|
||||
class="level-border clr-display-inline-block margin-right-5">
|
||||
<div>
|
||||
{{ 'SBOM.PACKAGES' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="noSbom">
|
||||
<span>{{ 'SBOM.NO_SBOM' | translate }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<hr />
|
||||
<div *ngIf="scanner">
|
||||
<span class="bar-scanning-time">{{
|
||||
'SCANNER.SCANNED_BY' | translate
|
||||
}}</span>
|
||||
<span class="margin-left-5">{{ getScannerInfo() }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{
|
||||
'SCANNER.DURATION' | translate
|
||||
}}</span>
|
||||
<span class="margin-left-5">{{ duration() }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time"
|
||||
>{{ 'SBOM.CHART.SCANNING_TIME' | translate }}
|
||||
</span>
|
||||
<span>{{ completeTimestamp | harborDatetime : 'short' }}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
<clr-tooltip class="margin-left-5" *ngIf="isLimitedSuccess()">
|
||||
<div clrTooltipTrigger>
|
||||
<clr-icon
|
||||
shape="exclamation-triangle"
|
||||
size="20"
|
||||
class="is-warning"></clr-icon>
|
||||
</div>
|
||||
<clr-tooltip-content [clrSize]="'lg'" *clrIfOpen>
|
||||
<div class="font-weight-600">
|
||||
<span class="bar-scanning-time"
|
||||
>{{ 'SBOM.CHART.SCANNING_PERCENT' | translate }}
|
||||
</span>
|
||||
<span>{{ completePercent }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{
|
||||
'SBOM.CHART.SCANNING_PERCENT_EXPLAIN' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
.tip-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-tooltip-font-larger {
|
||||
span {
|
||||
font-size: 16px;
|
||||
vertical-align: middle
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 0;
|
||||
border-color: #aaa;
|
||||
margin: 6px -10px;
|
||||
}
|
||||
|
||||
.font-weight-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.margin-left-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.width-215 {
|
||||
width: 215px;
|
||||
}
|
||||
|
||||
.width-150 {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.level-border>div{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
border-radius: 50%;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.tip-block {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.margin-right-5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SbomTipHistogramComponent } from './sbom-tip-histogram.component';
|
||||
import { of } from 'rxjs';
|
||||
import { Project } from '../../../../../../../app/base/project/project';
|
||||
import { Artifact } from 'ng-swagger-gen/models';
|
||||
|
||||
describe('SbomTipHistogramComponent', () => {
|
||||
let component: SbomTipHistogramComponent;
|
||||
let fixture: ComponentFixture<SbomTipHistogramComponent>;
|
||||
const mockRouter = {
|
||||
navigate: () => {},
|
||||
};
|
||||
const mockedArtifact: Artifact = {
|
||||
id: 123,
|
||||
type: 'IMAGE',
|
||||
};
|
||||
const mockedScanner = {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
};
|
||||
const mockActivatedRoute = {
|
||||
RouterparamMap: of({ get: key => 'value' }),
|
||||
snapshot: {
|
||||
params: {
|
||||
repo: 'test',
|
||||
digest: 'ABC',
|
||||
subscribe: () => {
|
||||
return of(null);
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
params: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
artifactResolver: [mockedArtifact, new Project()],
|
||||
},
|
||||
},
|
||||
data: of({
|
||||
projectResolver: {
|
||||
ismember: true,
|
||||
role_name: 'maintainer',
|
||||
},
|
||||
}),
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
TranslateService,
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
],
|
||||
declarations: [SbomTipHistogramComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SbomTipHistogramComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent basic functions', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.isLimitedSuccess()).toBeFalsy();
|
||||
expect(component.noSbom).toBeTruthy();
|
||||
expect(component.isThemeLight()).toBeFalsy();
|
||||
expect(component.duration()).toBe('0');
|
||||
expect(component.completePercent).toBe('0%');
|
||||
});
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent completeTimestamp', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
component.sbomSummary.end_time = new Date('2024-04-08 00:01:02');
|
||||
expect(component.completeTimestamp).toBe(
|
||||
component.sbomSummary.end_time
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test SbomTipHistogramComponent getScannerInfo', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.getScannerInfo()).toBe('');
|
||||
component.scanner = mockedScanner;
|
||||
expect(component.getScannerInfo()).toBe(
|
||||
`${mockedScanner.name}@${mockedScanner.version}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ScannerVo, SbomSummary } from '../../../../../../shared/services';
|
||||
import { SBOM_SCAN_STATUS } from '../../../../../../shared/units/utils';
|
||||
import {
|
||||
UN_LOGGED_PARAM,
|
||||
YES,
|
||||
} from '../../../../../../account/sign-in/sign-in.service';
|
||||
import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme';
|
||||
import { ScanTypes } from '../../../../../../shared/entities/shared.const';
|
||||
|
||||
const MIN = 60;
|
||||
const MIN_STR = 'min ';
|
||||
const SEC_STR = 'sec';
|
||||
const SUCCESS_PCT: number = 100;
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-sbom-tip-histogram',
|
||||
templateUrl: './sbom-tip-histogram.component.html',
|
||||
styleUrls: ['./sbom-tip-histogram.component.scss'],
|
||||
})
|
||||
export class SbomTipHistogramComponent {
|
||||
@Input() scanner: ScannerVo;
|
||||
@Input() sbomSummary: SbomSummary = {
|
||||
scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM,
|
||||
};
|
||||
@Input() artifactDigest: string = '';
|
||||
@Input() sbomDigest: string = '';
|
||||
constructor(
|
||||
private translate: TranslateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
duration(): string {
|
||||
if (this.sbomSummary && this.sbomSummary.duration) {
|
||||
let str = '';
|
||||
const min = Math.floor(this.sbomSummary.duration / MIN);
|
||||
if (min) {
|
||||
str += min + ' ' + MIN_STR;
|
||||
}
|
||||
const sec = this.sbomSummary.duration % MIN;
|
||||
if (sec) {
|
||||
str += sec + ' ' + SEC_STR;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
public get completePercent(): string {
|
||||
return this.sbomSummary.scan_status === SBOM_SCAN_STATUS.SUCCESS
|
||||
? `100%`
|
||||
: '0%';
|
||||
}
|
||||
isLimitedSuccess(): boolean {
|
||||
return (
|
||||
this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT
|
||||
);
|
||||
}
|
||||
get completeTimestamp(): Date {
|
||||
return this.sbomSummary && this.sbomSummary.end_time
|
||||
? this.sbomSummary.end_time
|
||||
: new Date();
|
||||
}
|
||||
|
||||
get noSbom(): boolean {
|
||||
return (
|
||||
this.sbomSummary.scan_status === SBOM_SCAN_STATUS.NOT_GENERATED_SBOM
|
||||
);
|
||||
}
|
||||
|
||||
isThemeLight() {
|
||||
return localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT;
|
||||
}
|
||||
|
||||
getScannerInfo(): string {
|
||||
if (this.scanner) {
|
||||
if (this.scanner.name && this.scanner.version) {
|
||||
return `${this.scanner.name}@${this.scanner.version}`;
|
||||
}
|
||||
if (this.scanner.name && !this.scanner.version) {
|
||||
return `${this.scanner.name}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
goIntoArtifactSbomSummaryPage(): void {
|
||||
const relativeRouterLink: string[] = ['artifacts', this.artifactDigest];
|
||||
if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) {
|
||||
this.router.navigate(relativeRouterLink, {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParams: {
|
||||
[UN_LOGGED_PARAM]: YES,
|
||||
sbomDigest: this.sbomDigest ?? '',
|
||||
tab: ScanTypes.SBOM,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(relativeRouterLink, {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParams: {
|
||||
sbomDigest: this.sbomDigest ?? '',
|
||||
tab: ScanTypes.SBOM,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
.bar-wrapper {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.bar-state {
|
||||
.unknow-text {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-chart {
|
||||
.loop-height {
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-error {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scanning-button {
|
||||
height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
top: -12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
|
||||
.bar-tooltip-font-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar-summary {
|
||||
margin-top: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-summary-item {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.bar-summary-item span:nth-child(1){
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bar-summary-item span:nth-child(2){
|
||||
width: 28px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.tip-icon-medium {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.tip-icon-low {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.font-color-green{
|
||||
color:green;
|
||||
}
|
||||
/* stylelint-disable */
|
||||
.bar-tooltip-font-larger span{
|
||||
font-size:16px;
|
||||
vertical-align:middle
|
||||
}
|
||||
|
||||
hr{
|
||||
border-bottom: 0;
|
||||
border-color: #aaa;
|
||||
margin: 6px -10px;
|
||||
}
|
||||
|
||||
.font-weight-600{
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.rightPos{
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 35px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.mt-3px {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.label-critical {
|
||||
background:#ff4d2e;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background:#ff8f3d!important;
|
||||
color:#000!important;
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
background-color: #ffce66;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-low {
|
||||
background: #fff1ad;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.label-none {
|
||||
background-color: #2ec0ff;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
hbr-vulnerability-bar {
|
||||
.label,.not-scan {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.stopped {
|
||||
border-color: #cccc15;
|
||||
}
|
||||
@@ -11,14 +11,43 @@ import {
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../../../../../shared/units/utils';
|
||||
import { NativeReportSummary } from '../../../../../../../ng-swagger-gen/models/native-report-summary';
|
||||
import { SharedTestingModule } from '../../../../../shared/shared.module';
|
||||
import { of, timer } from 'rxjs';
|
||||
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
||||
|
||||
describe('ResultBarChartComponent (inline template)', () => {
|
||||
let component: ResultBarChartComponent;
|
||||
let fixture: ComponentFixture<ResultBarChartComponent>;
|
||||
const mockedSbomDigest =
|
||||
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
||||
let mockData: NativeReportSummary = {
|
||||
report_id: '12345',
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
|
||||
severity: 'High',
|
||||
end_time: new Date().toUTCString(),
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.2',
|
||||
},
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
summary: {
|
||||
High: 5,
|
||||
Low: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
let mockCloneData: NativeReportSummary = {
|
||||
report_id: '123456',
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
|
||||
severity: 'High',
|
||||
end_time: new Date().toUTCString(),
|
||||
scanner: {
|
||||
name: 'Trivy',
|
||||
vendor: 'vm',
|
||||
version: 'v1.3',
|
||||
},
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
@@ -29,6 +58,59 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const FakedScanService = {
|
||||
scanArtifact: () => of({}),
|
||||
stopScanArtifact: () => of({}),
|
||||
};
|
||||
const FakedArtifactService = {
|
||||
getArtifact: () =>
|
||||
of({
|
||||
accessories: null,
|
||||
addition_links: {
|
||||
build_history: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/build_history',
|
||||
},
|
||||
vulnerabilities: {
|
||||
absolute: false,
|
||||
href: '/api/v2.0/projects/xuel/repositories/ui%252Fserver%252Fconfig-dev/artifacts/sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3/additions/vulnerabilities',
|
||||
},
|
||||
},
|
||||
digest: 'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3',
|
||||
extra_attrs: {
|
||||
architecture: 'amd64',
|
||||
author: '',
|
||||
config: {
|
||||
Env: [
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
],
|
||||
WorkingDir: '/',
|
||||
},
|
||||
created: '2024-01-10T10:05:33.2702206Z',
|
||||
os: 'linux',
|
||||
},
|
||||
icon: 'sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06',
|
||||
id: 3,
|
||||
labels: null,
|
||||
manifest_media_type:
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
media_type: 'application/vnd.docker.container.image.v1+json',
|
||||
project_id: 3,
|
||||
pull_time: '2024-04-02T01:50:58.332Z',
|
||||
push_time: '2024-03-06T09:47:08.163Z',
|
||||
references: null,
|
||||
repository_id: 2,
|
||||
scan_overview: {
|
||||
duration: 2,
|
||||
end_time: '2024-04-02T01:50:59.406Z',
|
||||
scan_status: 'Success',
|
||||
start_time: '2024-04-02T01:50:57.176Z',
|
||||
},
|
||||
size: 3957,
|
||||
tags: null,
|
||||
type: 'IMAGE',
|
||||
}),
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
@@ -43,6 +125,14 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
useValue: ScanningResultDefaultService,
|
||||
},
|
||||
{ provide: JobLogService, useValue: JobLogDefaultService },
|
||||
{
|
||||
provide: ScanService,
|
||||
useValue: FakedScanService,
|
||||
},
|
||||
{
|
||||
provide: ArtifactService,
|
||||
useValue: FakedArtifactService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -52,6 +142,8 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
component = fixture.componentInstance;
|
||||
component.artifactDigest = 'mockTag';
|
||||
component.summary = mockData;
|
||||
component.repoName = 'mockRepo';
|
||||
component.artifactDigest = mockedSbomDigest;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -109,4 +201,69 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
expect(el).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent getScanner', () => {
|
||||
fixture.detectChanges();
|
||||
component.summary = mockData;
|
||||
expect(component.getScanner()).toBe(mockData.scanner);
|
||||
component.projectName = 'test';
|
||||
component.repoName = 'ui';
|
||||
component.artifactDigest = 'dg';
|
||||
expect(component.viewLog()).toBe(
|
||||
'/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log'
|
||||
);
|
||||
component.copyValue(mockCloneData);
|
||||
expect(component.summary.report_id).toBe(mockCloneData.report_id);
|
||||
});
|
||||
it('Test ResultBarChartComponent status', () => {
|
||||
fixture.detectChanges();
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
expect(component.status).toBe(VULNERABILITY_SCAN_STATUS.SUCCESS);
|
||||
expect(component.completed).toBeTruthy();
|
||||
expect(component.queued).toBeFalsy();
|
||||
expect(component.scanning).toBeFalsy();
|
||||
expect(component.stopped).toBeFalsy();
|
||||
expect(component.otherStatus).toBeFalsy();
|
||||
expect(component.error).toBeFalsy();
|
||||
});
|
||||
it('Test ResultBarChartComponent ngOnDestroy', () => {
|
||||
component.stateCheckTimer = timer(0, 10000).subscribe(() => {});
|
||||
component.ngOnDestroy();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
expect(component.scanSubscription).toBeNull();
|
||||
expect(component.stopSubscription).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent scanNow', () => {
|
||||
fixture.detectChanges();
|
||||
component.scanNow();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onSubmitting).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent stopScan', () => {
|
||||
fixture.detectChanges();
|
||||
component.stopScan();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onStopping).toBeFalse();
|
||||
expect(component.stateCheckTimer).toBeNull();
|
||||
});
|
||||
});
|
||||
it('Test ResultBarChartComponent getSummary', () => {
|
||||
fixture.detectChanges();
|
||||
// component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
component.getSummary();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.summary.scan_status).toBe(
|
||||
VULNERABILITY_SCAN_STATUS.SUCCESS
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
HarborEvent,
|
||||
} from '../../../../../services/event-service/event.service';
|
||||
import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service';
|
||||
import { ScanType } from 'ng-swagger-gen/models';
|
||||
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
||||
|
||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||
const RETRY_TIMES: number = 3;
|
||||
@@ -110,7 +112,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
this.scanSubscription.unsubscribe();
|
||||
this.scanSubscription = null;
|
||||
}
|
||||
if (!this.stopSubscription) {
|
||||
if (this.stopSubscription) {
|
||||
this.stopSubscription.unsubscribe();
|
||||
this.stopSubscription = null;
|
||||
}
|
||||
@@ -171,6 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
// scanType: <ScanType>{
|
||||
// scan_type: ScanTypes.VULNERABILITY,
|
||||
// },
|
||||
})
|
||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||
.subscribe(
|
||||
@@ -286,6 +291,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
scanType: <ScanType>{
|
||||
scan_type: ScanTypes.VULNERABILITY,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 90%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,11 @@ export enum HarborEvent {
|
||||
SCROLL_TO_POSITION = 'scrollToPosition',
|
||||
REFRESH_PROJECT_INFO = 'refreshProjectInfo',
|
||||
START_SCAN_ARTIFACT = 'startScanArtifact',
|
||||
START_GENERATE_SBOM = 'startGenerateSbom',
|
||||
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
|
||||
STOP_SBOM_ARTIFACT = 'stopSbomArtifact',
|
||||
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
|
||||
UPDATE_SBOM_INFO = 'UpdateSbomInfo',
|
||||
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
|
||||
DELETE_ACCESSORY = 'deleteAccessory',
|
||||
COPY_DIGEST = 'copyDigest',
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ActivatedRouteSnapshot,
|
||||
} from '@angular/router';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { map, catchError, mergeMap } from 'rxjs/operators';
|
||||
import { catchError, mergeMap } from 'rxjs/operators';
|
||||
import { Artifact } from '../../../../ng-swagger-gen/models/artifact';
|
||||
import { ArtifactService } from '../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { Project } from '../../base/project/project';
|
||||
@@ -51,6 +51,7 @@ export class ArtifactDetailRoutingResolverService {
|
||||
projectName: project.name,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withTag: false,
|
||||
withImmutableStatus: true,
|
||||
}),
|
||||
|
||||
@@ -382,3 +382,8 @@ export const stringsForClarity: Partial<ClrCommonStrings> = {
|
||||
datepickerSelectYearText: 'CLARITY.DATE_PICKER_SELECT_YEAR_TEXT',
|
||||
datepickerSelectedLabel: 'CLARITY.DATE_PICKER_SELECTED_LABEL',
|
||||
};
|
||||
|
||||
export enum ScanTypes {
|
||||
SBOM = 'sbom',
|
||||
VULNERABILITY = 'vulnerability',
|
||||
}
|
||||
|
||||
@@ -211,6 +211,16 @@ export interface VulnerabilitySummary {
|
||||
scanner?: ScannerVo;
|
||||
complete_percent?: number;
|
||||
}
|
||||
export interface SbomSummary {
|
||||
report_id?: string;
|
||||
sbom_digest?: string;
|
||||
scan_status?: string;
|
||||
duration?: number;
|
||||
start_time?: Date;
|
||||
end_time?: Date;
|
||||
scanner?: ScannerVo;
|
||||
complete_percent?: number;
|
||||
}
|
||||
export interface ScannerVo {
|
||||
name?: string;
|
||||
vendor?: string;
|
||||
|
||||
@@ -105,6 +105,13 @@ export const USERSTATICPERMISSION = {
|
||||
READ: 'read',
|
||||
},
|
||||
},
|
||||
REPOSITORY_TAG_SBOM_JOB: {
|
||||
KEY: 'sbom',
|
||||
VALUE: {
|
||||
CREATE: 'create',
|
||||
READ: 'read',
|
||||
},
|
||||
},
|
||||
REPOSITORY_ARTIFACT_LABEL: {
|
||||
KEY: 'artifact-label',
|
||||
VALUE: {
|
||||
|
||||
@@ -275,6 +275,21 @@ export const VULNERABILITY_SCAN_STATUS = {
|
||||
SUCCESS: 'Success',
|
||||
SCHEDULED: 'Scheduled',
|
||||
};
|
||||
|
||||
/**
|
||||
* The state of sbom generation
|
||||
*/
|
||||
export const SBOM_SCAN_STATUS = {
|
||||
// front-end status
|
||||
NOT_GENERATED_SBOM: 'Not generated SBOM',
|
||||
// back-end status
|
||||
PENDING: 'Pending',
|
||||
RUNNING: 'Running',
|
||||
ERROR: 'Error',
|
||||
STOPPED: 'Stopped',
|
||||
SUCCESS: 'Success',
|
||||
SCHEDULED: 'Scheduled',
|
||||
};
|
||||
/**
|
||||
* The severity of vulnerability scanning
|
||||
*/
|
||||
|
||||
@@ -777,6 +777,7 @@
|
||||
"ARTIFACTS": "Artefakte",
|
||||
"SIZE": "Größe",
|
||||
"VULNERABILITY": "Schwachstellen",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Build History",
|
||||
"SIGNED": "Signiert",
|
||||
"AUTHOR": "Autor",
|
||||
@@ -1027,6 +1028,41 @@
|
||||
"IN_PROGRESS": "Suche...",
|
||||
"BACK": "Zurück"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Nicht gescannt",
|
||||
@@ -1107,6 +1143,7 @@
|
||||
"ALL": "Alle",
|
||||
"PLACEHOLDER": "Keine Artefakte gefunden!",
|
||||
"SCAN_UNSUPPORTED": "Nicht unterstützt",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Zusammenfassung",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
||||
@@ -778,6 +778,7 @@
|
||||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Build History",
|
||||
"SIGNED": "Signed",
|
||||
"AUTHOR": "Author",
|
||||
@@ -1028,6 +1029,41 @@
|
||||
"IN_PROGRESS": "Search...",
|
||||
"BACK": "Back"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
@@ -1108,6 +1144,7 @@
|
||||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Summary",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
||||
@@ -778,6 +778,7 @@
|
||||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Construir Historia",
|
||||
"SIGNED": "Firmada",
|
||||
"AUTHOR": "Autor",
|
||||
@@ -1026,6 +1027,41 @@
|
||||
"IN_PROGRESS": "Buscar...",
|
||||
"BACK": "Volver"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
@@ -1106,6 +1142,7 @@
|
||||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Summary",
|
||||
"DEPENDENCIES": "Dependencies",
|
||||
"VALUES": "Values",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -775,6 +775,7 @@
|
||||
"ARTIFACTS": "아티팩트들",
|
||||
"SIZE": "크기",
|
||||
"VULNERABILITY": "취약점",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "기록 생성",
|
||||
"SIGNED": "서명됨",
|
||||
"AUTHOR": "작성자",
|
||||
@@ -1025,6 +1026,41 @@
|
||||
"IN_PROGRESS": "검색 중...",
|
||||
"BACK": "뒤로"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "스캔되지 않음",
|
||||
@@ -1105,6 +1141,7 @@
|
||||
"ALL": "모두",
|
||||
"PLACEHOLDER": "아티팩트를 찾을 수 없음!",
|
||||
"SCAN_UNSUPPORTED": "지원되지 않음",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "요약",
|
||||
"DEPENDENCIES": "종속성",
|
||||
"VALUES": "값",
|
||||
|
||||
@@ -776,6 +776,7 @@
|
||||
"ARTIFACTS": "Artefatos",
|
||||
"SIZE": "Tamanho",
|
||||
"VULNERABILITY": "Vulnerabilidade",
|
||||
"SBOM": "SBOM",
|
||||
"SIGNED": "Assinada",
|
||||
"AUTHOR": "Autor",
|
||||
"CREATED": "Data de criação",
|
||||
@@ -1024,6 +1025,41 @@
|
||||
"IN_PROGRESS": "Buscando...",
|
||||
"BACK": "Voltar"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Não analisado",
|
||||
@@ -1104,6 +1140,7 @@
|
||||
"ALL": "Todos",
|
||||
"PLACEHOLDER": "Nenhum artefato encontrado!",
|
||||
"SCAN_UNSUPPORTED": "Não pode ser examinada",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Resumo",
|
||||
"DEPENDENCIES": "Dependências",
|
||||
"VALUES": "Valores",
|
||||
|
||||
@@ -777,6 +777,7 @@
|
||||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "Boyut",
|
||||
"VULNERABILITY": "Güvenlik Açığı",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "Geçmişi Oluştur",
|
||||
"SIGNED": "İmzalanmış",
|
||||
"AUTHOR": "Yazar",
|
||||
@@ -1027,6 +1028,41 @@
|
||||
"IN_PROGRESS": "Ara...",
|
||||
"BACK": "Geri"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Taranmadı",
|
||||
@@ -1107,6 +1143,7 @@
|
||||
"ALL": "All",
|
||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||
"SCAN_UNSUPPORTED": "Unsupported",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "Özet",
|
||||
"DEPENDENCIES": "Bağımlılıklar",
|
||||
"VALUES": "Değerler",
|
||||
|
||||
@@ -776,6 +776,7 @@
|
||||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "大小",
|
||||
"VULNERABILITY": "漏洞",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "构建历史",
|
||||
"SIGNED": "已签名",
|
||||
"AUTHOR": "作者",
|
||||
@@ -1025,6 +1026,41 @@
|
||||
"IN_PROGRESS": "搜索中...",
|
||||
"BACK": "返回"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "未扫描",
|
||||
@@ -1105,6 +1141,7 @@
|
||||
"ALL": "所有",
|
||||
"PLACEHOLDER": "未发现任何 artifacts!",
|
||||
"SCAN_UNSUPPORTED": "不支持扫描",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "概要",
|
||||
"DEPENDENCIES": "依赖",
|
||||
"VALUES": "取值",
|
||||
|
||||
@@ -776,6 +776,7 @@
|
||||
"ARTIFACTS": "Artifacts",
|
||||
"SIZE": "大小",
|
||||
"VULNERABILITY": "弱點",
|
||||
"SBOM": "SBOM",
|
||||
"BUILD_HISTORY": "建置歷史",
|
||||
"SIGNED": "已簽署",
|
||||
"AUTHOR": "作者",
|
||||
@@ -1024,6 +1025,41 @@
|
||||
"IN_PROGRESS": "搜尋中...",
|
||||
"BACK": "返回"
|
||||
},
|
||||
"SBOM": {
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed time:",
|
||||
"SCANNING_PERCENT": "Scan progress:",
|
||||
"SCANNING_PERCENT_EXPLAIN": "The scan completion progress is calculated as # of successfully scanned images / total number of images referenced within the image index.",
|
||||
"TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.",
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "No scan results found.",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_PACKAGES": "Packages",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_LICENSE": "License",
|
||||
"COLUMN_DESCRIPTION": "Description",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "Not Generated",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Generating",
|
||||
"STOPPED": "SBOM scan stopped"
|
||||
},
|
||||
"NO_SBOM": "No SBOM",
|
||||
"PACKAGES": "SBOM",
|
||||
"REPORTED_BY": "Reported by {{scanner}}",
|
||||
"GENERATE": "Create SBOM",
|
||||
"DOWNLOAD": "Download SBOM",
|
||||
"Details": "SBOM details",
|
||||
"STOP": "Stop SBOM",
|
||||
"TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"OTHER_STATUS": "未掃描",
|
||||
@@ -1104,6 +1140,7 @@
|
||||
"ALL": "全部",
|
||||
"PLACEHOLDER": "未發現任何 artifacts!",
|
||||
"SCAN_UNSUPPORTED": "不支援掃描",
|
||||
"SBOM_UNSUPPORTED": "Unsupported",
|
||||
"SUMMARY": "摘要",
|
||||
"DEPENDENCIES": "相依性",
|
||||
"VALUES": "值",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user