diff --git a/src/portal/angular.json b/src/portal/angular.json index 99e82e09b37..f717597043a 100644 --- a/src/portal/angular.json +++ b/src/portal/angular.json @@ -13,7 +13,8 @@ "options": { "allowedCommonJsDependencies": [ "cron-validator", - "js-yaml" + "js-yaml", + "highcharts" ], "outputPath": "dist", "index": "src/index.html", diff --git a/src/portal/package-lock.json b/src/portal/package-lock.json index 9b28f73dc0c..337e816caae 100644 --- a/src/portal/package-lock.json +++ b/src/portal/package-lock.json @@ -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", diff --git a/src/portal/package.json b/src/portal/package.json index 4c9204c3dba..6e79901866a 100644 --- a/src/portal/package.json +++ b/src/portal/package.json @@ -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" } } diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts b/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts index e987f9e37ac..54be381047e 100644 --- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts +++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.ts @@ -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); } } diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.component.html index 39b560da91a..46ea92d7e4e 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.component.html +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.component.html @@ -19,6 +19,14 @@

>{{ 'CONFIG.VULNERABILITY' | translate }} + diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.module.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.module.ts index b47b1d46c75..58837179037 100644 --- a/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.module.ts +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/interrogation-services.module.ts @@ -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, diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.html new file mode 100644 index 00000000000..04ca7487394 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.html @@ -0,0 +1,144 @@ + +

{{ 'SECURITY_HUB.VUL' | translate }}

+ + + + + + + + + {{ + 'SECURITY_HUB.CVE_ID' | translate + }} + {{ + 'SECURITY_HUB.REPO_NAME' | translate + }} + {{ + 'P2P_PROVIDER.DIGEST' | translate + }} + {{ 'AUDIT_LOG.TAGS' | translate }} + {{ 'VULNERABILITY.GRID.CVSS3' | translate }} + {{ + 'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate + }} + {{ + 'VULNERABILITY.PACKAGE' | translate + }} + {{ + 'VULNERABILITY.GRID.COLUMN_VERSION' | translate + }} + {{ + 'VULNERABILITY.GRID.COLUMN_FIXED' | translate + }} + {{ 'SECURITY_HUB.NO_VUL' | translate }} + + + + {{ + c.cve_id + }} + {{ c.cve_id }} + + {{ c.cve_id }} + + + + + + + + + {{ c.repository_name }} + + + {{ c?.digest?.slice(0, 15) }} + + {{ + c.tags?.join(', ') + }} + {{ c.cvss_v3_score }} + + {{ severityText(c.severity) | translate }} + {{ + severityText(c.severity) | translate + }} + {{ severityText(c.severity) | translate }} + {{ + severityText(c.severity) | translate + }} + {{ + severityText(c.severity) | translate + }} + {{ + severityText(c.severity) | translate + }} + + {{ + c.package + }} + {{ + c.version + }} + {{ + c.fixed_version + }} + + {{ 'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate }}: + {{ c.desc }} + + + + {{ total === -1 ? '1000+' : total }} + {{ 'SECURITY_HUB.CVE' | translate }} + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + + + diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.scss b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.scss new file mode 100644 index 00000000000..e94eb560824 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.spec.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.spec.ts new file mode 100644 index 00000000000..2b64497d629 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.spec.ts @@ -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; + 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> = + new HttpResponse>({ + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.ts new file mode 100644 index 00000000000..fa6eeca0271 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.component.ts @@ -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}`]); + } +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.interface.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.interface.ts new file mode 100644 index 00000000000..4f69ddf01b1 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/security-hub.interface.ts @@ -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)?$/; diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.html new file mode 100644 index 00000000000..a5e276f3643 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.html @@ -0,0 +1 @@ +
diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.scss b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.scss new file mode 100644 index 00000000000..cb5f33cc67f --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.scss @@ -0,0 +1,6 @@ +.single-bar { + width: 160px; + height: 80px; + position: absolute; + top: -12px; +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.spec.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.spec.ts new file mode 100644 index 00000000000..ac6a9ac7422 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.ts new file mode 100644 index 00000000000..849bf448fae --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/single-bar/single-bar.component.ts @@ -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', + }, + ], + }, + ], + }); + } +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.html new file mode 100644 index 00000000000..b019ae57d7d --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.html @@ -0,0 +1,127 @@ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ {{ 'BANNER_MESSAGE.FROM' | translate }} + + {{ 'BANNER_MESSAGE.TO' | translate }} + + {{ + 'SECURITY_HUB.INVALID_VALUE' | translate + }} +
+ + + + + +
+
+
+
+ +
diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.scss b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.scss new file mode 100644 index 00000000000..0c9e5d4dab9 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.scss @@ -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; + +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.spec.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.spec.ts new file mode 100644 index 00000000000..5fd7d830e13 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.ts new file mode 100644 index 00000000000..99f4e410825 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-filter/vulnerability-filter.component.ts @@ -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(); + 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; + } +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.html b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.html new file mode 100644 index 00000000000..408abb514c7 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.html @@ -0,0 +1,228 @@ +

+ {{ 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 }} +

+
+
+
+ {{ 'SECURITY_HUB.TOTAL_VUL' | translate }} +
+
+
+
+ +
+
+
+
+ +
+
+ {{ securitySummary?.critical_cnt || 0 }} +
+
+
+
+ +
+
+ {{ securitySummary?.high_cnt || 0 }} +
+
+
+
+ +
+
+ {{ securitySummary?.medium_cnt || 0 }} +
+
+
+
+ +
+
+ {{ securitySummary?.low_cnt || 0 }} +
+
+
+
+ +
+
+ {{ securitySummary?.unknown_cnt || 0 }} +
+
+
+
+ +
+
+ {{ securitySummary?.none_cnt || 0 }} +
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'SECURITY_HUB.TOP_5_ARTIFACT' | translate }} +
+
+
+ {{ 'SECURITY_HUB.REPO_NAME' | translate }} +
+
+ {{ 'P2P_PROVIDER.DIGEST' | translate }} +
+
+ {{ 'VULNERABILITY.PLURAL' | translate }} +
+
+
+ +
+
+
+
{{ 'SECURITY_HUB.TOP_5_CVE' | translate }}
+
+
+ {{ 'SECURITY_HUB.CVE_ID' | translate }} +
+
+ {{ 'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate }} +
+
+ {{ 'VULNERABILITY.GRID.CVSS3' | translate }} +
+
+ {{ 'VULNERABILITY.PACKAGE' | translate }} +
+
+
+
+
+ +
+ + {{ severityText(item.severity) | translate }} + {{ severityText(item.severity) | translate }} + {{ severityText(item.severity) | translate }} + {{ severityText(item.severity) | translate }} + {{ severityText(item.severity) | translate }} + {{ + severityText(item.severity) | translate + }} + +
+
+ {{ item?.cvss_score_v3 }} +
+
+ {{ item?.package + '@' + item?.version }} +
+
+
+
+
diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.scss b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.scss new file mode 100644 index 00000000000..94043c46b2e --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.spec.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.spec.ts new file mode 100644 index 00000000000..b3edacda7f5 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.ts b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.ts new file mode 100644 index 00000000000..1196b62416f --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/interrogation-services/vulnerability-database/vulnerability-summary/vulnerability-summary.component.ts @@ -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(); + @Output() + searchRepo = new EventEmitter(); + 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: '{point.percentage:.1f}%', + }, + 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'; + } +} diff --git a/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html b/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html index ebc3d50f019..4dd78313eef 100644 --- a/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html +++ b/src/portal/src/app/base/project/p2p-provider/add-p2p-policy/add-p2p-policy.component.html @@ -97,7 +97,7 @@