Add Security Hub UI (#18942)

1.Fixes #18819
2.Add Security Hub page as a new tab for Interrogation Services

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2023-07-20 10:54:07 +08:00 committed by GitHub
parent b08dce4805
commit 73533d8f4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 2114 additions and 48 deletions

View File

@ -13,7 +13,8 @@
"options": {
"allowedCommonJsDependencies": [
"cron-validator",
"js-yaml"
"js-yaml",
"highcharts"
],
"outputPath": "dist",
"index": "src/index.html",

View File

@ -25,6 +25,7 @@
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"cron-validator": "^1.3.1",
"highcharts": "^11.1.0",
"js-yaml": "^4.1.0",
"ngx-clipboard": "^15.1.0",
"ngx-cookie": "^6.0.1",
@ -10029,6 +10030,11 @@
"integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==",
"optional": true
},
"node_modules/highcharts": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.1.0.tgz",
"integrity": "sha512-vhmqq6/frteWMx0GKYWwEFL25g4OYc7+m+9KQJb/notXbNtIb8KVy+ijOF7XAFqF165cq0pdLIePAmyFY5ph3g=="
},
"node_modules/hosted-git-info": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz",

View File

@ -43,6 +43,7 @@
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"cron-validator": "^1.3.1",
"highcharts": "^11.1.0",
"js-yaml": "^4.1.0",
"ngx-clipboard": "^15.1.0",
"ngx-cookie": "^6.0.1",
@ -66,6 +67,7 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"cypress": "12.12.0",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
@ -86,7 +88,6 @@
"stylelint-config-prettier-scss": "^0.0.1",
"stylelint-config-standard": "^29.0.0",
"stylelint-config-standard-scss": "^6.1.0",
"typescript": "~5.0.4",
"cypress": "12.12.0"
"typescript": "~5.0.4"
}
}

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
OnDestroy,
ElementRef,
ChangeDetectorRef,
} from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { AppConfigService } from '../../services/app-config.service';
import { ModalEvent } from '../modal-event';
@ -192,5 +192,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
if (localStorage) {
localStorage.setItem(HAS_STYLE_MODE, this.styleMode);
}
this.event.publish(HarborEvent.THEME_CHANGE);
}
}

View File

@ -19,6 +19,14 @@
>{{ 'CONFIG.VULNERABILITY' | translate }}</a
>
</li>
<li class="nav-item">
<a
class="nav-link"
routerLink="security-hub"
routerLinkActive="active"
>{{ 'SECURITY_HUB.SECURITY_HUB' | translate }}</a
>
</li>
</ul>
</nav>
<router-outlet></router-outlet>

View File

@ -26,6 +26,10 @@ import {
ScanApiDefaultRepository,
ScanApiRepository,
} from './vulnerability/scanAll.api.repository';
import { VulnerabilitySummaryComponent } from './vulnerability-database/vulnerability-summary/vulnerability-summary.component';
import { VulnerabilityFilterComponent } from './vulnerability-database/vulnerability-filter/vulnerability-filter.component';
import { SecurityHubComponent } from './vulnerability-database/security-hub.component';
import { SingleBarComponent } from './vulnerability-database/single-bar/single-bar.component';
const routes: Routes = [
{
@ -40,6 +44,10 @@ const routes: Routes = [
path: 'vulnerability',
component: VulnerabilityConfigComponent,
},
{
path: 'security-hub',
component: SecurityHubComponent,
},
{
path: '',
redirectTo: 'scanners',
@ -57,6 +65,10 @@ const routes: Routes = [
ConfigurationScannerComponent,
InterrogationServicesComponent,
VulnerabilityConfigComponent,
VulnerabilityFilterComponent,
VulnerabilitySummaryComponent,
SecurityHubComponent,
SingleBarComponent,
],
providers: [
ScanAllRepoService,

View File

@ -0,0 +1,144 @@
<app-vulnerability-summary
(searchCVE)="searchCVE($event)"
(searchRepo)="searchRepo($event)"></app-vulnerability-summary>
<h1 appScrollSection="{{ vulId }}">{{ 'SECURITY_HUB.VUL' | translate }}</h1>
<clr-datagrid
[clrDgLoading]="loading"
(clrDgRefresh)="clrDgRefresh($event, options)">
<clr-dg-action-bar class="action-bar">
<app-vulnerability-filter
[loading]="loading"
(search)="search($event)"></app-vulnerability-filter>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</clr-dg-action-bar>
<clr-dg-column class="min-width">{{
'SECURITY_HUB.CVE_ID' | translate
}}</clr-dg-column>
<clr-dg-column class="min-width">{{
'SECURITY_HUB.REPO_NAME' | translate
}}</clr-dg-column>
<clr-dg-column class="min-width">{{
'P2P_PROVIDER.DIGEST' | translate
}}</clr-dg-column>
<clr-dg-column>{{ 'AUDIT_LOG.TAGS' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'VULNERABILITY.GRID.CVSS3' | translate }}</clr-dg-column>
<clr-dg-column>{{
'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate
}}</clr-dg-column>
<clr-dg-column class="min-width">{{
'VULNERABILITY.PACKAGE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'VULNERABILITY.GRID.COLUMN_VERSION' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'VULNERABILITY.GRID.COLUMN_FIXED' | translate
}}</clr-dg-column>
<clr-dg-placeholder
>{{ 'SECURITY_HUB.NO_VUL' | translate }}
</clr-dg-placeholder>
<clr-dg-row *ngFor="let c of vul" [clrDgItem]="c">
<clr-dg-cell class="min-width">
<span *ngIf="!c?.links || c?.links?.length === 0">{{
c.cve_id
}}</span>
<a
rel="noopener noreferrer"
*ngIf="c?.links && c?.links.length === 1"
href="{{ c?.links[0] }}"
target="_blank"
>{{ c.cve_id }}</a
>
<span *ngIf="c?.links && c?.links.length > 1">
{{ c.cve_id }}
<clr-signpost>
<clr-signpost-content *clrIfOpen>
<div class="mt-5px" *ngFor="let link of c?.links">
<a
rel="noopener noreferrer"
href="{{ link }}"
target="_blank"
>{{ link }}</a
>
</div>
</clr-signpost-content>
</clr-signpost>
</span>
</clr-dg-cell>
<clr-dg-cell class="ellipsis min-width" title="{{ c.repository_name }}">
<a
href="javascript:void(0)"
[routerLink]="getRepoLink(c.project_id, c.repository_name)"
>{{ c.repository_name }}</a
>
</clr-dg-cell>
<clr-dg-cell class="ellipsis min-width" title="{{ c?.digest }}">
<a
href="javascript:void(0)"
[routerLink]="
getDigestLink(c.project_id, c.repository_name, c.digest)
"
>{{ c?.digest?.slice(0, 15) }}</a
>
</clr-dg-cell>
<clr-dg-cell class="ellipsis" title="{{ c.tags?.join(', ') }}">{{
c.tags?.join(', ')
}}</clr-dg-cell>
<clr-dg-cell>{{ c.cvss_v3_score }}</clr-dg-cell>
<clr-dg-cell [ngSwitch]="c.severity">
<span
*ngSwitchCase="'Critical'"
class="label label-critical no-border"
>{{ severityText(c.severity) | translate }}</span
>
<span *ngSwitchCase="'High'" class="label label-danger no-border">{{
severityText(c.severity) | translate
}}</span>
<span
*ngSwitchCase="'Medium'"
class="label label-medium no-border"
>{{ severityText(c.severity) | translate }}</span
>
<span *ngSwitchCase="'Low'" class="label label-low no-border">{{
severityText(c.severity) | translate
}}</span>
<span *ngSwitchCase="'None'" class="label label-none no-border">{{
severityText(c.severity) | translate
}}</span>
<span *ngSwitchDefault>{{
severityText(c.severity) | translate
}}</span>
</clr-dg-cell>
<clr-dg-cell class="ellipsis" title="{{ c.package }}">{{
c.package
}}</clr-dg-cell>
<clr-dg-cell class="ellipsis" title="{{ c.version }}">{{
c.version
}}</clr-dg-cell>
<clr-dg-cell class="ellipsis" title="{{ c.fixed_version }}">{{
c.fixed_version
}}</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
{{ 'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate }}:
{{ c.desc }}
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span class="mr-1"
>{{ total === -1 ? '1000+' : total }}
{{ 'SECURITY_HUB.CVE' | translate }}</span
>
<clr-dg-pagination
#pagination
[clrDgTotalItems]="total === -1 ? maxNum : total"
[clrDgPageSize]="pageSize"
[(clrDgPage)]="currentPage">
<clr-dg-page-size [clrPageSizeOptions]="[10, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,66 @@
.flex {
display: flex;
}
.label-critical {
background:red;
color:#621501;
}
.label-danger {
background:#e64524!important;
color:#621501!important;
}
.label-medium {
background-color: orange;
color:#621501;
}
.label-low {
background: #007CBB;
color:#cab6b1;
}
.label-none {
background-color: grey;
color:#bad7ba;
}
.no-border {
border: none;
}
.action-bar {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.refresh-btn {
margin-right: 2rem;
cursor: pointer;
&:hover {
color: #007CBB;
}
}
.repo-name {
min-width: 10rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.min-width {
min-width: 9rem !important;
}

View File

@ -0,0 +1,198 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SecurityHubComponent } from './security-hub.component';
import { of } from 'rxjs';
import { delay, finalize } from 'rxjs/operators';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { SecurityhubService } from '../../../../../../ng-swagger-gen/services/securityhub.service';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { VulnerabilityItem } from '../../../../../../ng-swagger-gen/models/vulnerability-item';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('SecurityHubComponent', () => {
let component: SecurityHubComponent;
let fixture: ComponentFixture<SecurityHubComponent>;
const mockedVuls: VulnerabilityItem[] = [
{
cve_id: 'CVE-2021-44228',
cvss_v3_score: 10,
desc: 'Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.',
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
fixed_version: '2.3.2, 2.12.2, 2.15.0',
links: ['https://avd.aquasec.com/nvd/cve-2021-44228'],
package: 'org.apache.logging.log4j:log4j-core',
project_id: 11,
repository_name: 'sample/nuxeo',
severity: 'Critical',
tags: [],
version: '2.11.1',
},
{
cve_id: 'CVE-2021-44228',
cvss_v3_score: 10,
desc: 'Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.',
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
fixed_version: '2.3.2, 2.12.2, 2.15.0',
links: ['https://avd.aquasec.com/nvd/cve-2021-44228'],
package: 'org.apache.logging.log4j:log4j-core',
project_id: 1,
repository_name: 'library/nuxeo',
severity: 'Critical',
tags: ['v2.3.0'],
version: '2.11.1',
},
{
cve_id: 'CVE-2021-21345',
cvss_v3_score: 9.9,
desc: "XStream is a Java library to serialize objects to XML and back again. In XStream before version 1.4.16, there is a vulnerability which may allow a remote attacker who has sufficient rights to execute commands of the host only by manipulating the processed input stream. No user is affected, who followed the recommendation to setup XStream's security framework with a whitelist limited to the minimal required types. If you rely on XStream's default blacklist of the Security Framework, you will have to use at least version 1.4.16.",
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
fixed_version: '1.4.16',
links: ['https://avd.aquasec.com/nvd/cve-2021-21345'],
package: 'com.thoughtworks.xstream:xstream',
project_id: 1,
repository_name: 'library/nuxeo',
severity: 'Critical',
tags: ['v2.3.0'],
version: '1.4.10',
},
{
cve_id: 'CVE-2021-21345',
cvss_v3_score: 9.9,
desc: "XStream is a Java library to serialize objects to XML and back again. In XStream before version 1.4.16, there is a vulnerability which may allow a remote attacker who has sufficient rights to execute commands of the host only by manipulating the processed input stream. No user is affected, who followed the recommendation to setup XStream's security framework with a whitelist limited to the minimal required types. If you rely on XStream's default blacklist of the Security Framework, you will have to use at least version 1.4.16.",
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
fixed_version: '1.4.16',
links: ['https://avd.aquasec.com/nvd/cve-2021-21345'],
package: 'com.thoughtworks.xstream:xstream',
project_id: 11,
repository_name: 'sample/nuxeo',
severity: 'Critical',
tags: [],
version: '1.4.10',
},
{
cve_id: 'CVE-2020-27619',
cvss_v3_score: 9.8,
desc: 'In Python 3 through 3.9.0, the Lib/test/multibytecodec_support.py CJK codec tests call eval() on content retrieved via HTTP.',
digest: 'sha256:c7c1c56aab2d5b0f1470ec90d7113665ed577d6952b48b88f556e3448a9a4175',
links: ['https://avd.aquasec.com/nvd/cve-2020-27619'],
package: 'libpython3.9-stdlib',
project_id: 1,
repository_name: 'library/spectral',
severity: 'Low',
tags: ['v6.1.0'],
version: '3.9.2-1',
},
{
cve_id: 'CVE-2020-27619',
cvss_v3_score: 9.8,
desc: 'In Python 3 through 3.9.0, the Lib/test/multibytecodec_support.py CJK codec tests call eval() on content retrieved via HTTP.',
digest: 'sha256:c7c1c56aab2d5b0f1470ec90d7113665ed577d6952b48b88f556e3448a9a4175',
links: ['https://avd.aquasec.com/nvd/cve-2020-27619'],
package: 'libpython3.9-minimal',
project_id: 1,
repository_name: 'library/spectral',
severity: 'Low',
tags: ['v6.1.0'],
version: '3.9.2-1',
},
{
cve_id: 'CVE-2022-37454',
cvss_v3_score: 9.8,
desc: 'The Keccak XKCP SHA-3 reference implementation before fdc6fef has an integer overflow and resultant buffer overflow that allows attackers to execute arbitrary code or eliminate expected cryptographic properties. This occurs in the sponge function interface.',
digest: 'sha256:c7c1c56aab2d5b0f1470ec90d7113665ed577d6952b48b88f556e3448a9a4175',
links: ['https://avd.aquasec.com/nvd/cve-2022-37454'],
package: 'libpython3.9-stdlib',
project_id: 1,
repository_name: 'library/spectral',
severity: 'Low',
tags: ['v6.1.0'],
version: '3.9.2-1',
},
{
cve_id: 'CVE-2019-1010022',
cvss_v3_score: 9.8,
desc: '** DISPUTED ** GNU Libc current is affected by: Mitigation bypass. The impact is: Attacker may bypass stack guard protection. The component is: nptl. The attack vector is: Exploit stack buffer overflow vulnerability and use this bypass vulnerability to bypass stack guard. NOTE: Upstream comments indicate "this is being treated as a non-security bug and no real threat."',
digest: 'sha256:d2b2f2980e9ccc570e5726b56b54580f23a018b7b7314c9eaff7e5e479c78657',
links: ['https://avd.aquasec.com/nvd/cve-2019-1010022'],
package: 'libc6',
project_id: 6,
repository_name: 'dockerhub-proxy-cache/library/nginx',
severity: 'Low',
tags: [],
version: '2.36-9',
},
{
cve_id: 'CVE-2019-1010022',
cvss_v3_score: 9.8,
desc: '** DISPUTED ** GNU Libc current is affected by: Mitigation bypass. The impact is: Attacker may bypass stack guard protection. The component is: nptl. The attack vector is: Exploit stack buffer overflow vulnerability and use this bypass vulnerability to bypass stack guard. NOTE: Upstream comments indicate "this is being treated as a non-security bug and no real threat."',
digest: 'sha256:c7c1c56aab2d5b0f1470ec90d7113665ed577d6952b48b88f556e3448a9a4175',
links: ['https://avd.aquasec.com/nvd/cve-2019-1010022'],
package: 'libc6',
project_id: 1,
repository_name: 'library/spectral',
severity: 'Low',
tags: ['v6.1.0'],
version: '2.31-13+deb11u6',
},
{
cve_id: 'CVE-2017-9117',
cvss_v3_score: 9.8,
desc: 'In LibTIFF 4.0.7, the program processes BMP images without verifying that biWidth and biHeight in the bitmap-information header match the actual input, leading to a heap-based buffer over-read in bmp2tiff.',
digest: 'sha256:d2b2f2980e9ccc570e5726b56b54580f23a018b7b7314c9eaff7e5e479c78657',
links: ['https://avd.aquasec.com/nvd/cve-2017-9117'],
package: 'libtiff6',
project_id: 6,
repository_name: 'dockerhub-proxy-cache/library/nginx',
severity: 'Low',
tags: [],
version: '4.5.0-6',
},
];
const fakedSecurityhubService = {
ListVulnerabilitiesResponse() {
const res: HttpResponse<Array<VulnerabilityItem>> =
new HttpResponse<Array<VulnerabilityItem>>({
headers: new HttpHeaders({ 'x-total-count': '-1' }),
body: mockedVuls,
});
return of(res)
.pipe(delay(0))
.pipe(
finalize(() => {
component.loading = false;
})
);
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
imports: [SharedTestingModule],
declarations: [SecurityHubComponent],
providers: [
{
provide: SecurityhubService,
useValue: fakedSecurityhubService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(SecurityHubComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render vulnerabilities', async () => {
fixture.detectChanges();
await fixture.whenStable();
fixture.autoDetectChanges(true);
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(10);
});
});

View File

@ -0,0 +1,170 @@
import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { SecurityhubService } from '../../../../../../ng-swagger-gen/services/securityhub.service';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { ClrDatagridStateInterface } from '@clr/angular/data/datagrid/interfaces/state.interface';
import {
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from '../../../../shared/units/utils';
import { finalize } from 'rxjs/operators';
import { VulnerabilityItem } from '../../../../../../ng-swagger-gen/models/vulnerability-item';
import {
OptionType,
SearchEventData,
severityText,
VUL_ID,
getDigestLink,
getRepoLink,
} from './security-hub.interface';
import { ProjectService } from '../../../../../../ng-swagger-gen/services/project.service';
import { VulnerabilityFilterComponent } from './vulnerability-filter/vulnerability-filter.component';
@Component({
selector: 'app-security-hub',
templateUrl: './security-hub.component.html',
styleUrls: ['./security-hub.component.scss'],
})
export class SecurityHubComponent {
loading: boolean = true;
currentPage: number = 1;
pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.SECURITY_HUB_VUL,
10
);
total: number = 0;
vul: VulnerabilityItem[] = [];
state: ClrDatagridStateInterface;
options: string[] = [];
readonly maxNum: number = Number.MAX_SAFE_INTEGER;
readonly vulId: string = VUL_ID;
readonly severityText = severityText;
readonly getDigestLink = getDigestLink;
readonly getRepoLink = getRepoLink;
@ViewChild('pagination', { static: true })
pagination: any;
@ViewChild(VulnerabilityFilterComponent, { static: true })
vulnerabilityFilterComponent: VulnerabilityFilterComponent;
constructor(
private securityHubService: SecurityhubService,
private messageHandler: MessageHandlerService,
private projectService: ProjectService,
private cd: ChangeDetectorRef
) {}
clrDgRefresh(state: ClrDatagridStateInterface, searchOption: string[]) {
if (state && state.page) {
this.pageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.SECURITY_HUB_VUL,
this.pageSize
);
}
this.loading = true;
this.state = state;
this.options = searchOption;
this.securityHubService
.ListVulnerabilitiesResponse({
tuneCount: true,
withTag: true,
page: this.currentPage,
pageSize: this.pageSize,
q: encodeURIComponent(
this.options?.length ? this.options.join(',') : ''
),
})
.pipe(finalize(() => (this.loading = false)))
.subscribe({
next: res => {
if (res.headers) {
const xHeader: string =
res.headers.get('X-Total-Count');
if (xHeader) {
this.total = parseInt(xHeader, 0);
this.cd.detectChanges();
this.updateTotalPage();
}
this.vul = res.body;
}
},
error: err => {
this.messageHandler.error(err);
},
});
}
search(res: SearchEventData) {
if (res?.projectId) {
this.projectService
.getProject({
projectNameOrId: res.projectId,
})
.subscribe({
next: project => {
if (project?.project_id) {
res.normal.push(
`${OptionType.PROJECT_ID}=${project?.project_id}`
);
this.clrDgRefresh(this.state, res?.normal);
} else {
res.normal.push(`${OptionType.PROJECT_ID}=0`);
this.clrDgRefresh(this.state, res?.normal);
}
},
error: err => {
this.messageHandler.error(err);
},
});
} else {
this.clrDgRefresh(this.state, res?.normal);
}
}
refresh() {
if (!this.loading) {
this.currentPage = 1;
this.clrDgRefresh(this.state, this.options);
}
}
// Use hack way to update total page element
updateTotalPage() {
const span: HTMLSpanElement = document.querySelector(
'app-security-hub clr-datagrid clr-dg-pagination .pagination-list>span'
);
const lastPageBtn: HTMLButtonElement = document.querySelector(
'app-security-hub clr-datagrid clr-dg-pagination .pagination-last'
);
if (this.total === -1) {
if (span) {
span.innerText = Math.ceil(1000 / this.pageSize) + '+';
}
if (lastPageBtn) {
lastPageBtn.disabled = true;
}
} else {
if (span) {
span.innerText = Math.ceil(
this.total / this.pageSize
).toString();
}
if (lastPageBtn) {
lastPageBtn.disabled = false;
}
}
}
searchCVE(cveId: string) {
this.vulnerabilityFilterComponent.selectedOptions = [OptionType.CVE_ID];
this.vulnerabilityFilterComponent.valueMap[OptionType.CVE_ID] = cveId;
this.currentPage = 1;
this.clrDgRefresh(this.state, [`${OptionType.CVE_ID}=${cveId}`]);
}
searchRepo(repoName: string) {
this.vulnerabilityFilterComponent.selectedOptions = [OptionType.REPO];
this.vulnerabilityFilterComponent.valueMap[OptionType.REPO] = repoName;
this.currentPage = 1;
this.clrDgRefresh(this.state, [`${OptionType.REPO}=${repoName}`]);
}
}

View File

@ -0,0 +1,93 @@
import { VULNERABILITY_SEVERITY } from '../../../../shared/units/utils';
export const SEVERITY_OPTIONS = [
{
severity: 'Critical',
severityLevel: 'VULNERABILITY.SEVERITY.CRITICAL',
},
{ severity: 'High', severityLevel: 'VULNERABILITY.SEVERITY.HIGH' },
{ severity: 'Medium', severityLevel: 'VULNERABILITY.SEVERITY.MEDIUM' },
{ severity: 'Low', severityLevel: 'VULNERABILITY.SEVERITY.LOW' },
{ severity: 'Unknown', severityLevel: 'UNKNOWN' },
{ severity: 'None', severityLevel: 'VULNERABILITY.SEVERITY.NONE' },
];
export enum OptionType {
ALL = 'all',
CVE_ID = 'cve_id',
SEVERITY = 'severity',
CVSS3 = 'cvss_score_v3',
REPO = 'repository_name',
PACKAGE = 'package',
TAG = 'tag',
PROJECT_ID = 'project_id',
}
export const OptionType_I18n_Map = {
[OptionType.ALL]: 'SECURITY_HUB.OPTION_ALL',
[OptionType.CVE_ID]: 'SECURITY_HUB.CVE_ID',
[OptionType.SEVERITY]: 'VULNERABILITY.GRID.COLUMN_SEVERITY',
[OptionType.CVSS3]: 'VULNERABILITY.GRID.CVSS3',
[OptionType.REPO]: 'SECURITY_HUB.REPO_NAME',
[OptionType.PACKAGE]: 'VULNERABILITY.PACKAGE',
[OptionType.TAG]: 'REPLICATION.TAG',
[OptionType.PROJECT_ID]: 'SECURITY_HUB.OPTION_PROJECT_ID_NAME',
};
export interface OptionTypeValueMap {
[key: string]: any;
}
export interface SearchEventData {
normal: string[];
projectId: string;
}
export const VUL_ID: string = 'vulnerabilities';
export function severityText(severity: string): string {
switch (severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
return 'VULNERABILITY.SEVERITY.CRITICAL';
case VULNERABILITY_SEVERITY.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VULNERABILITY_SEVERITY.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VULNERABILITY_SEVERITY.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VULNERABILITY_SEVERITY.NONE:
return 'VULNERABILITY.SEVERITY.NONE';
default:
return 'UNKNOWN';
}
}
export function getDigestLink(
proId: number | string,
repoName: string,
digest: string
): any {
const projectName = repoName?.split('/')[0];
const realRepoName = projectName
? repoName?.substring(projectName.length + 1)
: repoName;
return [
'/harbor/projects',
proId,
'repositories',
realRepoName,
'artifacts-tab',
'artifacts',
digest,
];
}
export function getRepoLink(proId: number | string, repoName: string): any {
const projectName = repoName?.split('/')[0];
const realRepoName = projectName
? repoName?.substring(projectName.length + 1)
: repoName;
return ['/harbor/projects', proId, 'repositories', realRepoName];
}
export const CVSS3_REG = /^([0-9]|10)(\.\d)?$/;

View File

@ -0,0 +1 @@
<div class="single-bar" #container></div>

View File

@ -0,0 +1,6 @@
.single-bar {
width: 160px;
height: 80px;
position: absolute;
top: -12px;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SingleBarComponent } from './single-bar.component';
import { SharedTestingModule } from '../../../../../shared/shared.module';
describe('VulnerabilityDetailsComponent', () => {
let component: SingleBarComponent;
let fixture: ComponentFixture<SingleBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SharedTestingModule],
declarations: [SingleBarComponent],
}).compileComponents();
fixture = TestBed.createComponent(SingleBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,94 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnChanges,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { DangerousArtifact } from '../../../../../../../ng-swagger-gen/models/dangerous-artifact';
import * as Highcharts from 'highcharts';
@Component({
selector: 'app-single-bar',
templateUrl: './single-bar.component.html',
styleUrls: ['./single-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SingleBarComponent implements OnChanges {
@Input()
dangerousArtifact: DangerousArtifact;
@ViewChild('container', { static: true })
container: ElementRef;
ngOnChanges(changes: SimpleChanges) {
if (changes && changes['dangerousArtifact']) {
this.initChart();
}
}
initChart() {
(Highcharts as any).chart(this.container.nativeElement, {
credits: {
enabled: false,
},
chart: {
backgroundColor: 'transparent',
type: 'bar',
},
title: {
text: '',
},
tooltip: {
pointFormat: '{series.data.name}{point.y}',
style: {
fontSize: 12,
whiteSpace: 'nowrap',
},
},
plotOptions: {
pie: {
startAngle: -90,
endAngle: 90,
dataLabels: {
enabled: true,
distance: -8,
style: {
fontSize: 8,
fontWeight: 1,
},
pointFormat: '{point.y}',
},
size: 50,
borderWidth: 0,
borderRadius: 2,
},
},
series: [
{
type: 'pie',
name: 'Severity',
data: [
{
name: 'Critical',
y: this.dangerousArtifact?.critical_cnt || 0,
color: 'red',
},
{
name: 'High',
y: this.dangerousArtifact?.high_cnt || 0,
color: '#e64524',
},
{
name: 'Medium',
y: this.dangerousArtifact?.medium_cnt || 0,
color: 'orange',
},
],
},
],
});
}
}

View File

@ -0,0 +1,127 @@
<div class="clr-row">
<form class="clr-form clr-form-horizontal">
<ng-container *ngFor="let item of selectedOptions; let i = index">
<div class="clr-form-control">
<label class="clr-control-label">
<ng-container *ngIf="i === 0"
>{{ 'SECURITY_HUB.FILTER_BY' | translate
}}<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<span>{{
'SECURITY_HUB.TOOLTIP' | translate
}}</span>
</clr-tooltip-content>
</clr-tooltip></ng-container
>
</label>
<div class="clr-control-container flex">
<div class="clr-select-wrapper">
<select
class="clr-select"
(change)="select()"
[(ngModel)]="selectedOptions[i]"
[ngModelOptions]="{ standalone: true }">
<option
value="{{ OptionType.ALL }}"
*ngIf="i === 0">
{{
OptionType_I18n_Map[OptionType.ALL]
| translate
}}
</option>
<option
value="{{ item }}"
*ngFor="
let item of getOption(selectedOptions[i])
">
{{ OptionType_I18n_Map[item] | translate }}
</option>
</select>
</div>
<div
class="clr-input-wrapper ml-1"
*ngIf="
selectedOptions[i] !== OptionType.ALL &&
selectedOptions[i] !== OptionType.SEVERITY &&
selectedOptions[i] !== OptionType.CVSS3
">
<input
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="valueMap[selectedOptions[i]]"
class="clr-input"
type="text" />
</div>
<div
class="clr-select-wrapper ml-1"
*ngIf="selectedOptions[i] === OptionType.SEVERITY">
<select
class="clr-select"
[(ngModel)]="severity"
[ngModelOptions]="{ standalone: true }">
<option
value="{{ item.severity }}"
*ngFor="let item of SEVERITY_OPTIONS">
{{ item.severityLevel | translate }}
</option>
</select>
</div>
<div
class="clr-input-wrapper ml-1"
[class.clr-error]="isInvalid()"
*ngIf="selectedOptions[i] === OptionType.CVSS3">
<span>{{ 'BANNER_MESSAGE.FROM' | translate }}</span>
<input
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="startScore"
class="clr-input"
type="text" />
<span>{{ 'BANNER_MESSAGE.TO' | translate }}</span>
<input
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="endScore"
class="clr-input"
type="text" />
<clr-control-error *ngIf="isInvalid()">{{
'SECURITY_HUB.INVALID_VALUE' | translate
}}</clr-control-error>
</div>
<ng-container
*ngIf="
selectedOptions[i] !== OptionType.ALL && i === 0
">
<clr-icon
shape="plus-circle"
class="plus"
[class.disabled]="!canAdd()"
(click)="add()"></clr-icon>
<clr-icon
[class.disabled]="!canReduce()"
(click)="reduce()"
shape="minus-circle"
class="minus"></clr-icon>
</ng-container>
</div>
</div>
</ng-container>
</form>
<button
(click)="fireSearchEvent()"
id="search"
type="button"
class="btn btn-primary ml-1"
[disabled]="loading || isInvalid()"
[clrLoading]="loading">
<clr-icon shape="search"></clr-icon
>{{ 'SECURITY_HUB.SEARCH' | translate }}
</button>
</div>

View File

@ -0,0 +1,54 @@
$input-and-select-width: 8rem;
.flex {
display: flex;
}
.clr-control-container {
align-items: center;
}
.clr-select {
min-width: $input-and-select-width;
margin-top: 1px;
}
.clr-input-wrapper {
min-width: $input-and-select-width;
}
.clr-control-label {
width: 4rem !important;
}
.plus {
margin-left: 2rem;
background-color: green;
border-radius: 50%;
color: white;
cursor: pointer;
}
.minus {
margin-left: 1rem;
background-color: red;
border-radius: 50%;
color: white;
cursor: pointer;
}
.clr-form {
padding-left: 0.75rem;
}
.disabled {
cursor: not-allowed;
background-color: gray;
}
.clr-row {
align-items: baseline;
}

View File

@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VulnerabilityFilterComponent } from './vulnerability-filter.component';
import { OptionType } from '../security-hub.interface';
import { SharedTestingModule } from '../../../../../shared/shared.module';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('VulnerabilityFilterComponent', () => {
let component: VulnerabilityFilterComponent;
let fixture: ComponentFixture<VulnerabilityFilterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
imports: [SharedTestingModule],
declarations: [VulnerabilityFilterComponent],
}).compileComponents();
fixture = TestBed.createComponent(VulnerabilityFilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('"All" is selected by default', () => {
fixture.detectChanges();
const select: HTMLSelectElement =
fixture.nativeElement.querySelector('select');
expect(select.value).toEqual(OptionType.ALL);
});
});

View File

@ -0,0 +1,136 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import {
CVSS3_REG,
OptionType,
OptionType_I18n_Map,
OptionTypeValueMap,
SearchEventData,
SEVERITY_OPTIONS,
} from '../security-hub.interface';
@Component({
selector: 'app-vulnerability-filter',
templateUrl: './vulnerability-filter.component.html',
styleUrls: ['./vulnerability-filter.component.scss'],
})
export class VulnerabilityFilterComponent {
severity: string;
selectedOptions: string[] = ['all'];
candidates: string[] = [
OptionType.CVE_ID,
OptionType.SEVERITY,
OptionType.CVSS3,
OptionType.PROJECT_ID,
OptionType.REPO,
OptionType.PACKAGE,
OptionType.TAG,
];
allOptions: string[] = [
OptionType.CVE_ID,
OptionType.SEVERITY,
OptionType.CVSS3,
OptionType.PROJECT_ID,
OptionType.REPO,
OptionType.PACKAGE,
OptionType.TAG,
];
valueMap: OptionTypeValueMap = {};
startScore: string;
endScore: string;
@Output()
search = new EventEmitter<SearchEventData>();
readonly SEVERITY_OPTIONS = SEVERITY_OPTIONS;
readonly OptionType = OptionType;
readonly OptionType_I18n_Map = OptionType_I18n_Map;
@Input()
loading: boolean = false;
constructor() {}
select() {
if (this.selectedOptions[0] === 'all') {
this.selectedOptions = ['all'];
}
this.candidates = this.allOptions.filter(item => {
return !this.selectedOptions.find(item2 => item2 === item);
});
}
add() {
if (this.canAdd()) {
this.selectedOptions.push(this.candidates[0]);
this.candidates.shift();
}
}
reduce() {
if (this.canReduce()) {
this.candidates.unshift(
this.selectedOptions[this.selectedOptions.length - 1]
);
this.selectedOptions.pop();
}
}
canAdd(): boolean {
return this.selectedOptions.length < 7;
}
canReduce(): boolean {
return this.selectedOptions.length >= 2;
}
getOption(currentOption: string): string[] {
if (currentOption === 'all') {
return this.candidates;
}
return [currentOption].concat(this.candidates);
}
fireSearchEvent() {
let result: SearchEventData = {
normal: [],
projectId: '',
};
this.selectedOptions.forEach(item => {
if (item === OptionType.ALL) {
this.search.emit(result);
return;
} else if (
item === OptionType.PROJECT_ID &&
this.valueMap[OptionType.PROJECT_ID]
) {
result.projectId = this.valueMap[OptionType.PROJECT_ID];
} else if (item === OptionType.SEVERITY) {
if (this.severity) {
result.normal.push(
`${OptionType.SEVERITY}=${this.severity}`
);
}
} else if (item === OptionType.CVSS3) {
if (this.startScore || this.endScore) {
result.normal.push(
`${OptionType.CVSS3}=[${
this.startScore ? this.startScore : '0.0'
}~${this.endScore ? this.endScore : '10.0'}]`
);
}
} else if (this.valueMap[item]) {
result.normal.push(`${item}=${this.valueMap[item]}`);
}
});
this.search.emit(result);
}
isInvalid(): boolean {
if (this.selectedOptions.indexOf(OptionType.CVSS3) !== -1) {
if (this.startScore && !CVSS3_REG.test(this.startScore)) {
return true;
}
if (this.endScore && !CVSS3_REG.test(this.endScore)) {
return true;
}
if (this.startScore && this.endScore) {
return +this.startScore > +this.endScore;
}
}
return false;
}
}

View File

@ -0,0 +1,228 @@
<h2>
{{ securitySummary?.total_artifact || 0 }}
{{ 'SECURITY_HUB.ARTIFACTS' | translate }},
{{ securitySummary?.scanned_cnt || 0 }}
{{ 'SECURITY_HUB.SCANNED' | translate }},
{{ securitySummary?.total_artifact - securitySummary?.scanned_cnt }}
{{ 'SECURITY_HUB.NOT_SCANNED' | translate }}
</h2>
<div class="container">
<div class="card">
<div class="card-header text-truncate">
{{ 'SECURITY_HUB.TOTAL_VUL' | translate }}
</div>
<div class="card-block">
<div class="clr-row">
<div class="clr-col center">
<label class="card-header text-truncate sub-header-title">
{{
'SECURITY_HUB.TOTAL_AND_FIXABLE'
| translate
: {
totalNum:
securitySummary?.total_vuls || 0,
fixableNum:
securitySummary?.fixable_cnt || 0
}
}}
</label>
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3 center">
<label class="text-truncate"
>{{ 'VULNERABILITY.SEVERITY.CRITICAL' | translate }}
</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.critical_cnt || 0 }}
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3">
<label class="text-truncate">{{
'VULNERABILITY.SEVERITY.HIGH' | translate
}}</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.high_cnt || 0 }}
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3">
<label class="text-truncate">{{
'VULNERABILITY.SEVERITY.MEDIUM' | translate
}}</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.medium_cnt || 0 }}
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3">
<label class="text-truncate">{{
'VULNERABILITY.SEVERITY.LOW' | translate
}}</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.low_cnt || 0 }}
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3">
<label class="text-truncate">{{
'UNKNOWN' | translate
}}</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.unknown_cnt || 0 }}
</div>
</div>
<div class="clr-row severity-item">
<div class="clr-col-3">
<label class="text-truncate">{{
'VULNERABILITY.SEVERITY.NONE' | translate
}}</label>
</div>
<div class="clr-col-9">
{{ securitySummary?.none_cnt || 0 }}
</div>
</div>
<div class="clr-row">
<div class="placeholder">
<div class="pie-chart" id="pie-chart"></div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header pb-0">
<div class="mb-1">
{{ 'SECURITY_HUB.TOP_5_ARTIFACT' | translate }}
</div>
<div class="clr-row">
<div class="clr-col column">
{{ 'SECURITY_HUB.REPO_NAME' | translate }}
</div>
<div class="clr-col column">
{{ 'P2P_PROVIDER.DIGEST' | translate }}
</div>
<div class="clr-col column">
{{ 'VULNERABILITY.PLURAL' | translate }}
</div>
</div>
</div>
<div class="card-block pt-1">
<div
class="clr-row row"
*ngFor="let item of securitySummary?.dangerous_artifacts">
<div class="clr-col ellipsis">
<a
class="search"
href="javascript:void(0)"
appScrollAnchor="{{ vulId }}"
(click)="searchRepoClick(item?.repository_name)"
title="{{ item.repository_name }}"
><clr-icon shape="search"></clr-icon
>{{ item.repository_name }}</a
>
</div>
<div class="clr-col" title="{{ item?.digest }}">
<a
href="javascript:void(0)"
[routerLink]="
getDigestLink(
item?.project_id,
item?.repository_name,
item.digest
)
"
>{{ item?.digest?.slice(0, 15) }}</a
>
</div>
<div class="clr-col">
<div class="single-bar-container">
<app-single-bar
[dangerousArtifact]="item"></app-single-bar>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header pb-0">
<div class="mb-1">{{ 'SECURITY_HUB.TOP_5_CVE' | translate }}</div>
<div class="clr-row">
<div class="clr-col-4 column">
{{ 'SECURITY_HUB.CVE_ID' | translate }}
</div>
<div class="clr-col-2 column">
{{ 'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate }}
</div>
<div class="clr-col-2 column">
{{ 'VULNERABILITY.GRID.CVSS3' | translate }}
</div>
<div class="clr-col-4 column">
{{ 'VULNERABILITY.PACKAGE' | translate }}
</div>
</div>
</div>
<div class="card-block pt-1">
<div
class="clr-row row"
*ngFor="let item of securitySummary?.dangerous_cves">
<div class="clr-col-4 ellipsis">
<a
class="search"
href="javascript:void(0)"
appScrollAnchor="{{ vulId }}"
(click)="searchCVEClick(item?.cve_id)"
title="{{ item.cve_id }}"
><clr-icon shape="search"></clr-icon
>{{ item.cve_id }}</a
>
</div>
<div class="clr-col-2">
<ng-container [ngSwitch]="item.severity">
<span
*ngSwitchCase="'Critical'"
class="label label-critical no-border"
>{{ severityText(item.severity) | translate }}</span
>
<span
*ngSwitchCase="'High'"
class="label label-danger no-border"
>{{ severityText(item.severity) | translate }}</span
>
<span
*ngSwitchCase="'Medium'"
class="label label-medium no-border"
>{{ severityText(item.severity) | translate }}</span
>
<span
*ngSwitchCase="'Low'"
class="label label-low no-border"
>{{ severityText(item.severity) | translate }}</span
>
<span
*ngSwitchCase="'None'"
class="label label-none no-border"
>{{ severityText(item.severity) | translate }}</span
>
<span *ngSwitchDefault>{{
severityText(item.severity) | translate
}}</span>
</ng-container>
</div>
<div class="clr-col-2">
{{ item?.cvss_score_v3 }}
</div>
<div
class="clr-col-4 ellipsis"
title="{{ item?.package + '@' + item?.version }}">
{{ item?.package + '@' + item?.version }}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,96 @@
$row-height: 48px;
.sub-header-title {
padding: 0 !important;
}
.container {
display: flex;
flex-wrap: wrap;
}
.card {
max-width: 32%;
min-width: 20rem;
flex-grow:0;
flex-shrink:0;
height: 17rem;
}
.card:not(:last-child) {
margin-right: 1rem;
}
.placeholder {
position: relative;
width: 100%;
}
.pie-chart {
position: absolute;
top: -6rem;
width: 100%;
height: 200px;
}
.column {
font-size: 10px;
font-weight: bolder;
text-transform: uppercase;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.single-bar-container {
position: relative;
height: $row-height;
width: 60px;
}
.card-header {
height: 4rem;
}
.row {
height: $row-height;
}
.search {
display: flex;
align-items: center;
}
.label-critical {
background:red;
color:#621501;
}
.label-danger {
background:#e64524!important;
color:#621501!important;
}
.label-medium {
background-color: orange;
color:#621501;
}
.label-low {
background: #007CBB;
color:#cab6b1;
}
.label-none {
background-color: grey;
color:#bad7ba;
}
.no-border {
border: none;
}

View File

@ -0,0 +1,140 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VulnerabilitySummaryComponent } from './vulnerability-summary.component';
import { SharedTestingModule } from '../../../../../shared/shared.module';
import { SecurityhubService } from '../../../../../../../ng-swagger-gen/services/securityhub.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { SecuritySummary } from '../../../../../../../ng-swagger-gen/models/security-summary';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('VulnerabilitySummaryComponent', () => {
let component: VulnerabilitySummaryComponent;
let fixture: ComponentFixture<VulnerabilitySummaryComponent>;
const mockedSummary: SecuritySummary = {
critical_cnt: 323,
dangerous_artifacts: [
{
critical_cnt: 124,
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
high_cnt: 903,
medium_cnt: 861,
project_id: 1,
repository_name: 'library/nuxeo',
},
{
critical_cnt: 124,
digest: 'sha256:7027e69a2172e38cef8ac2cb1f046025895c9fcf3160e8f70ffb26446f680e4d',
high_cnt: 903,
medium_cnt: 861,
project_id: 11,
repository_name: 'sample/nuxeo',
},
{
critical_cnt: 64,
digest: 'sha256:b7b209fce05e70ccd2e0358114264355cd7df0dd464bb5b23ac41b6215653a22',
high_cnt: 149,
medium_cnt: 147,
project_id: 1,
repository_name: 'library/openldap',
},
{
critical_cnt: 8,
digest: 'sha256:c7c1c56aab2d5b0f1470ec90d7113665ed577d6952b48b88f556e3448a9a4175',
high_cnt: 104,
medium_cnt: 80,
project_id: 1,
repository_name: 'library/spectral',
},
{
critical_cnt: 3,
digest: 'sha256:a97a153152fcd6410bdf4fb64f5622ecf97a753f07dcc89dab14509d059736cf',
high_cnt: 31,
medium_cnt: 28,
project_id: 1,
repository_name: 'library/nuxeo',
},
],
dangerous_cves: [
{
cve_id: 'CVE-2021-44228',
cvss_score_v3: 10,
package: 'org.apache.logging.log4j:log4j-core',
severity: 'Critical',
version: '2.11.1',
},
{
cve_id: 'CVE-2021-21345',
cvss_score_v3: 9.9,
package: 'com.thoughtworks.xstream:xstream',
severity: 'Critical',
version: '1.4.10',
},
{
cve_id: 'CVE-2018-7648',
cvss_score_v3: 9.8,
package: 'libopenjp2-7',
severity: 'Low',
version: '2.3.0-2+deb10u2',
},
{
cve_id: 'CVE-2023-34152',
cvss_score_v3: 9.8,
package: 'libmagickcore-6.q16-6',
severity: 'Low',
version: '8:6.9.10.23+dfsg-2.1+deb10u1',
},
{
cve_id: 'CVE-2020-35527',
cvss_score_v3: 9.8,
package: 'libsqlite3-0',
severity: 'Critical',
version: '3.27.2-3+deb10u1',
},
],
fixable_cnt: 3937,
high_cnt: 2191,
low_cnt: 2385,
medium_cnt: 2132,
scanned_cnt: 41,
total_artifact: 41,
total_vuls: 7115,
unknown_cnt: 84,
};
const fakedSecurityhubService = {
getSecuritySummary() {
return of(mockedSummary).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
imports: [SharedTestingModule],
declarations: [VulnerabilitySummaryComponent],
providers: [
{
provide: SecurityhubService,
useValue: fakedSecurityhubService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(VulnerabilitySummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', async () => {
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const cards = fixture.nativeElement.querySelectorAll('.card');
expect(cards.length).toEqual(3);
});
});

View File

@ -0,0 +1,186 @@
import {
Component,
EventEmitter,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { SecurityhubService } from '../../../../../../../ng-swagger-gen/services/securityhub.service';
import { SecuritySummary } from '../../../../../../../ng-swagger-gen/models/security-summary';
import { MessageHandlerService } from '../../../../../shared/services/message-handler.service';
import * as Highcharts from 'highcharts';
import highchartsAccessibility from 'highcharts/modules/accessibility';
import { getDigestLink, severityText, VUL_ID } from '../security-hub.interface';
import { HAS_STYLE_MODE, StyleMode } from '../../../../../services/theme';
import { Subscription } from 'rxjs';
import {
EventService,
HarborEvent,
} from '../../../../../services/event-service/event.service';
import { TranslateService } from '@ngx-translate/core';
highchartsAccessibility(Highcharts);
@Component({
selector: 'app-vulnerability-summary',
templateUrl: './vulnerability-summary.component.html',
styleUrls: ['./vulnerability-summary.component.scss'],
})
export class VulnerabilitySummaryComponent implements OnInit, OnDestroy {
@Output()
searchCVE = new EventEmitter<string>();
@Output()
searchRepo = new EventEmitter<string>();
securitySummary: SecuritySummary;
readonly vulId: string = VUL_ID;
readonly severityText = severityText;
readonly getDigestLink = getDigestLink;
harborEventSub: Subscription;
constructor(
private securityHubService: SecurityhubService,
private messageHandler: MessageHandlerService,
private event: EventService,
private translate: TranslateService
) {}
ngOnInit() {
this.getSummary();
if (!this.harborEventSub) {
this.harborEventSub = this.event.subscribe(
HarborEvent.THEME_CHANGE,
() => {
if (this.securitySummary) {
this.setOption(this.securitySummary);
}
}
);
}
}
ngOnDestroy() {
if (this.harborEventSub) {
this.harborEventSub.unsubscribe();
this.harborEventSub = null;
}
}
getSummary() {
this.securityHubService
.getSecuritySummary({
withDangerousArtifact: true,
withDangerousCve: true,
})
.subscribe({
next: res => {
this.securitySummary = res;
this.setOption(res);
},
error: err => {
this.messageHandler.error(err);
},
});
}
setOption(summary: SecuritySummary) {
const [severity, c, h, m, l, n, u] = [
'VULNERABILITY.GRID.COLUMN_SEVERITY',
'VULNERABILITY.SEVERITY.CRITICAL',
'VULNERABILITY.SEVERITY.HIGH',
'VULNERABILITY.SEVERITY.MEDIUM',
'VULNERABILITY.SEVERITY.LOW',
'VULNERABILITY.SEVERITY.NONE',
'UNKNOWN',
];
this.translate.get([severity, c, h, m, l, n, u]).subscribe(res => {
Highcharts.chart('pie-chart', {
credits: {
enabled: false,
},
chart: {
backgroundColor: 'transparent',
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie',
},
title: {
text: '',
},
tooltip: {
pointFormat: '<b>{point.percentage:.1f}%</b>',
},
plotOptions: {
pie: {
dataLabels: {
enabled: false,
},
showInLegend: true,
},
},
legend: {
align: 'left',
floating: true,
symbolRadius: 2,
itemStyle: {
fontSize: '12px',
fontWeight: '100',
color: this.getColorByTheme(),
},
width: '60%',
},
series: [
{
innerSize: '60%',
name: res[severity],
type: 'pie',
center: ['80%', '50%'],
data: [
{
name: res[c],
y: summary?.critical_cnt || 0,
color: 'red',
},
{
name: res[h],
y: summary?.high_cnt || 0,
color: '#e64524',
},
{
name: res[m],
y: summary?.medium_cnt || 0,
color: 'orange',
},
{
name: res[l],
y: summary?.low_cnt || 0,
color: '#007CBB',
},
{
name: res[u],
y: summary?.unknown_cnt || 0,
color: 'grey',
},
{
name: res[n],
y: summary?.none_cnt || 0,
color: 'green',
},
],
},
],
});
});
}
searchCVEClick(cveId: string) {
this.searchCVE.emit(cveId);
}
searchRepoClick(repoName: string) {
this.searchRepo.emit(repoName);
}
getColorByTheme(): string {
return localStorage?.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT
? '#000'
: '#fff';
}
}

View File

@ -22,7 +22,6 @@ import {
PageSizeMapKeys,
setPageSizeToLocalStorage,
SEVERITY_LEVEL_MAP,
VULNERABILITY_SEVERITY,
} from '../../../../../../shared/units/utils';
import { ResultBarChartComponent } from '../../vulnerability-scanning/result-bar-chart.component';
import { Subscription } from 'rxjs';
@ -32,6 +31,7 @@ import {
EventService,
HarborEvent,
} from '../../../../../../services/event-service/event.service';
import { severityText } from '../../../../../left-side-nav/interrogation-services/vulnerability-database/security-hub.interface';
@Component({
selector: 'hbr-artifact-vulnerabilities',
@ -73,6 +73,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
PageSizeMapKeys.ARTIFACT_VUL_COMPONENT,
25
);
readonly severityText = severityText;
constructor(
private errorHandler: ErrorHandler,
private additionsService: AdditionsService,
@ -237,23 +238,6 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
this.getVulnerabilities();
}
severityText(severity: string): string {
switch (severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
return 'VULNERABILITY.SEVERITY.CRITICAL';
case VULNERABILITY_SEVERITY.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VULNERABILITY_SEVERITY.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VULNERABILITY_SEVERITY.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VULNERABILITY_SEVERITY.NONE:
return 'VULNERABILITY.SEVERITY.NONE';
default:
return 'UNKNOWN';
}
}
scanNow() {
this.onSendingScanCommand = true;
this.eventService.publish(

View File

@ -9,6 +9,10 @@ import { Project } from '../../project';
import { artifactDefault } from './artifact';
import { SafeUrl } from '@angular/platform-browser';
import { ArtifactService } from './artifact.service';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
@Component({
selector: 'artifact-summary',
@ -38,7 +42,8 @@ export class ArtifactSummaryComponent implements OnInit {
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private router: Router,
private frontEndArtifactService: ArtifactService
private frontEndArtifactService: ArtifactService,
private event: EventService
) {}
goBack(): void {
@ -109,6 +114,8 @@ export class ArtifactSummaryComponent implements OnInit {
this.getIconFromBackEnd();
}
}
// scroll to the top for harbor container HTML element
this.event.publish(HarborEvent.SCROLL_TO_POSITION, 0);
}
onBack(): void {
this.backEvt.emit(this.repositoryName);

View File

@ -53,20 +53,6 @@
</li>
</ul>
</div>
<div class="pt-05 flex" *ngIf="withHelmChart">
<h4 class="mt-0 title-width">
{{ 'SUMMARY.PROJECT_HELM_CHART' | translate }}
</h4>
<ul class="list-unstyled">
<li>
{{
summaryInformation?.chart_count
? summaryInformation?.chart_count
: 0
}}
</li>
</ul>
</div>
<div *ngIf="showProjectMemberInfo" class="pt-05 flex">
<h4 class="mt-0 title-width">
{{ 'SUMMARY.PROJECT_MEMBER' | translate }}

View File

@ -83,4 +83,5 @@ export enum HarborEvent {
COPY_DIGEST = 'copyDigest',
REFRESH_BANNER_MESSAGE = 'refreshBannerMessage',
RETRIEVED_ICON = 'retrievedIcon',
THEME_CHANGE = 'themeChange',
}

View File

@ -17,6 +17,7 @@ import { Repository } from '../../../../../ng-swagger-gen/models/repository';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { SessionService } from '../../services/session.service';
import { UN_LOGGED_PARAM, YES } from '../../../account/sign-in/sign-in.service';
import { getRepoLink } from '../../../base/left-side-nav/interrogation-services/vulnerability-database/security-hub.interface';
@Component({
selector: 'list-repository-ro',
@ -25,20 +26,13 @@ import { UN_LOGGED_PARAM, YES } from '../../../account/sign-in/sign-in.service';
})
export class ListRepositoryROComponent {
@Input() repositories: Repository[];
readonly getLink = getRepoLink;
constructor(
private router: Router,
private searchTrigger: SearchTriggerService,
private sessionService: SessionService
) {}
getLink(projectId: number, repoName: string) {
let projectName = repoName.split('/')[0];
let repositorieName = projectName
? repoName.substr(projectName.length + 1)
: repoName;
return ['/harbor/projects', projectId, 'repositories', repositorieName];
}
getQueryParams() {
if (this.sessionService.getCurrentUser()) {
return null;

View File

@ -0,0 +1,9 @@
import { ScrollManagerService } from './scroll-manager.service';
import { ScrollAnchorDirective } from './scroll-anchor.directive';
describe('ScrollAnchorDirective', () => {
it('should create an instance', () => {
const directive = new ScrollAnchorDirective(new ScrollManagerService());
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { ScrollManagerService } from './scroll-manager.service';
import { Directive, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appScrollAnchor]',
})
export class ScrollAnchorDirective {
@Input('appScrollAnchor') id: string | number;
constructor(private manager: ScrollManagerService) {}
@HostListener('click')
scroll() {
this.manager.scroll(this.id);
}
}

View File

@ -0,0 +1,8 @@
import { ScrollManagerService } from './scroll-manager.service';
describe('ScrollManagerDirective', () => {
it('should create an instance', () => {
const directive = new ScrollManagerService();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { ScrollSectionDirective } from './scroll-section.directive';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ScrollManagerService {
private sections = new Map<string | number, ScrollSectionDirective>();
scroll(id: string | number) {
this.sections.get(id)!.scroll();
}
register(section: ScrollSectionDirective) {
this.sections.set(section.id, section);
}
remove(section: ScrollSectionDirective) {
this.sections.delete(section.id);
}
}

View File

@ -0,0 +1,12 @@
import { ScrollManagerService } from './scroll-manager.service';
import { ScrollSectionDirective } from './scroll-section.directive';
describe('ScrollSectionDirective', () => {
it('should create an instance', () => {
const directive = new ScrollSectionDirective(
{ nativeElement: <HTMLDivElement>document.createElement('div') },
new ScrollManagerService()
);
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,28 @@
import { ScrollManagerService } from './scroll-manager.service';
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
@Directive({
selector: '[appScrollSection]',
})
export class ScrollSectionDirective implements OnInit, OnDestroy {
@Input('appScrollSection') id: string | number;
constructor(
private host: ElementRef<HTMLElement>,
private manager: ScrollManagerService
) {}
ngOnInit() {
this.manager.register(this);
}
ngOnDestroy() {
this.manager.remove(this);
}
scroll() {
this.host.nativeElement.scrollIntoView({
behavior: 'smooth',
});
}
}

View File

@ -75,6 +75,9 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HarborDatetimePipe } from './pipes/harbor-datetime.pipe';
import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component';
import { LabelSelectorComponent } from './components/label-selector/label-selector.component';
import { ScrollSectionDirective } from './directives/scroll/scroll-section.directive';
import { ScrollManagerService } from './directives/scroll/scroll-manager.service';
import { ScrollAnchorDirective } from './directives/scroll/scroll-anchor.directive';
// ClarityIcons is publicly accessible from the browser's window object.
declare const ClarityIcons: ClarityIconsApi;
@ -115,6 +118,8 @@ ClarityIcons.add({
MaxLengthExtValidatorDirective,
PortValidatorDirective,
DateValidatorDirective,
ScrollSectionDirective,
ScrollAnchorDirective,
InlineAlertComponent,
NewUserFormComponent,
MessageComponent,
@ -154,6 +159,8 @@ ClarityIcons.add({
MaxLengthExtValidatorDirective,
PortValidatorDirective,
DateValidatorDirective,
ScrollSectionDirective,
ScrollAnchorDirective,
InlineAlertComponent,
NewUserFormComponent,
MessageComponent,

View File

@ -1042,4 +1042,5 @@ export enum PageSizeMapKeys {
WORKER_LIST_COMPONENT_WORKER = 'WorkerListComponentWorker',
SCHEDULE_LIST_COMPONENT = 'ScheduleListComponent',
PENDING_LIST_COMPONENT = 'PendingListComponent',
SECURITY_HUB_VUL = 'SecurityHubComponent',
}

View File

@ -1873,5 +1873,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1874,5 +1874,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project Id or Name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1870,5 +1870,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1840,5 +1840,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1870,5 +1870,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1873,5 +1873,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}

View File

@ -1870,5 +1870,26 @@
"WARNING": "警告",
"DANGER": "危险",
"ENTER_MESSAGE": "请输入消息内容"
},
"SECURITY_HUB": {
"SECURITY_HUB": "安全中心",
"ARTIFACTS": "Artifact(s)",
"SCANNED": "已扫描",
"NOT_SCANNED": "未扫描",
"TOTAL_VUL": "漏洞总览",
"TOTAL_AND_FIXABLE": "{{totalNum}} 个漏洞中 {{fixableNum}} 可修复",
"TOP_5_ARTIFACT": "最危险的5个 Artifacts",
"TOP_5_CVE": "最危险的5个 CVEs",
"CVE_ID": "CVE ID",
"VUL": "漏洞",
"CVE": "CVEs",
"FILTER_BY": "过滤条件",
"OPTION_ALL": "全部",
"OPTION_PROJECT_ID_NAME": "项目 ID 或 名称",
"SEARCH": "搜索",
"REPO_NAME": "仓库名称",
"TOOLTIP": "CVSS3 除外的所有过滤项只支持精确匹配",
"NO_VUL": "未找到任何漏洞",
"INVALID_VALUE": "无效范围"
}
}

View File

@ -1862,5 +1862,26 @@
"WARNING": "Warning",
"DANGER": "Danger",
"ENTER_MESSAGE": "Enter your message here"
},
"SECURITY_HUB": {
"SECURITY_HUB": "Security Hub",
"ARTIFACTS": "artifact(s)",
"SCANNED": "scanned",
"NOT_SCANNED": "not scanned",
"TOTAL_VUL": "Total Vulnerabilities",
"TOTAL_AND_FIXABLE": "{{totalNum}} total with {{fixableNum}} fixable",
"TOP_5_ARTIFACT": "Top 5 Most Dangerous Artifacts",
"TOP_5_CVE": "Top 5 Most Dangerous CVEs",
"CVE_ID": "CVE ID",
"VUL": "Vulnerabilities",
"CVE": "CVEs",
"FILTER_BY": "Filter by",
"OPTION_ALL": "All",
"OPTION_PROJECT_ID_NAME": "Project id or name",
"SEARCH": "SEARCH",
"REPO_NAME": "Repository Name",
"TOOLTIP": "All filters except CVSS3 only support exact matches",
"NO_VUL": "We could not find any vulnerability",
"INVALID_VALUE": "Invalid range"
}
}