feat: As a user I want to have an findings overview

This commit is contained in:
Marcel Haag 2022-10-24 12:48:47 +02:00 committed by Cel
parent 6764583481
commit 747cade495
32 changed files with 660 additions and 78 deletions

View File

@ -9,13 +9,16 @@ import {MomentModule} from 'ngx-moment';
import {NotificationService} from '../shared/services/notification.service'; import {NotificationService} from '../shared/services/notification.service';
import {NbToastrModule} from '@nebular/theme'; import {NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '../assets/@theme/theme.module'; import {ThemeModule} from '../assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http); return new TranslateHttpLoader(http);
} }
@NgModule({ @NgModule({
declarations: [], declarations: [
LoadingSpinnerComponent
],
imports: [ imports: [
CommonModule, CommonModule,
NbToastrModule, // used for notification service NbToastrModule, // used for notification service
@ -37,6 +40,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
NotificationService NotificationService
], ],
exports: [ exports: [
LoadingSpinnerComponent,
// modules // modules
MomentModule MomentModule
] ]

View File

@ -15,7 +15,7 @@
<nb-actions size="medium"> <nb-actions size="medium">
<nb-action> <nb-action>
<button nbButton hero <button nbButton hero
status="primary" status="info"
shape="round" shape="round"
(click)="onClickExportPentest()"> (click)="onClickExportPentest()">
<fa-icon [icon]="fa.faFileExport" <fa-icon [icon]="fa.faFileExport"

View File

@ -21,6 +21,7 @@ import {FormsModule} from '@angular/forms';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {CommonAppModule} from '../common-app.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -48,7 +49,8 @@ import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-s
NbListModule, NbListModule,
FontAwesomeModule, FontAwesomeModule,
FlexLayoutModule, FlexLayoutModule,
NbActionsModule NbActionsModule,
CommonAppModule
], ],
exports: [ exports: [
ObjectiveHeaderComponent, ObjectiveHeaderComponent,

View File

@ -54,5 +54,5 @@
</table> </table>
</nb-card> </nb-card>
<!--ToDo: Add loading spinner after routing fix to avoid circular dependency issues <!--ToDo: Add loading spinner after routing fix to avoid circular dependency issues -->
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>--> <app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -1,6 +1,6 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {Pentest, PentestEntry, transformPentestsToEntries} from '@shared/models/pentest.model'; import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state'; import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state';
@ -21,21 +21,21 @@ import {ChangePentest} from '@shared/stores/project-state/project-state.actions'
export class ObjectiveTableComponent implements OnInit { export class ObjectiveTableComponent implements OnInit {
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
columns: Array<PentestColumns> = [PentestColumns.TEST_ID, PentestColumns.TITLE, PentestColumns.STATUS, PentestColumns.FINDINGS]; columns: Array<ObjectiveColumns> = [ObjectiveColumns.TEST_ID, ObjectiveColumns.TITLE, ObjectiveColumns.STATUS, ObjectiveColumns.FINDINGS];
dataSource: NbTreeGridDataSource<PentestEntry>; dataSource: NbTreeGridDataSource<ObjectiveEntry>;
private data: PentestEntry[] = []; private data: ObjectiveEntry[] = [];
getters: NbGetters<PentestEntry, PentestEntry> = { getters: NbGetters<ObjectiveEntry, ObjectiveEntry> = {
dataGetter: (node: PentestEntry) => node, dataGetter: (node: ObjectiveEntry) => node,
childrenGetter: (node: PentestEntry) => node.childEntries || undefined, childrenGetter: (node: ObjectiveEntry) => node.childEntries || undefined,
expandedGetter: (node: PentestEntry) => !!node.expanded, expandedGetter: (node: ObjectiveEntry) => !!node.expanded,
}; };
constructor( constructor(
private store: Store, private store: Store,
private pentestService: PentestService, private pentestService: PentestService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<PentestEntry>, private dataSourceBuilder: NbTreeGridDataSourceBuilder<ObjectiveEntry>,
private readonly router: Router private readonly router: Router
) { ) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters); this.dataSource = dataSourceBuilder.create(this.data, this.getters);
@ -53,7 +53,7 @@ export class ObjectiveTableComponent implements OnInit {
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (pentests: Pentest[]) => { next: (pentests: Pentest[]) => {
this.data = transformPentestsToEntries(pentests); this.data = transformPentestsToObjectiveEntries(pentests);
this.dataSource.setData(this.data, this.getters); this.dataSource.setData(this.data, this.getters);
this.loading$.next(false); this.loading$.next(false);
}, },
@ -88,7 +88,7 @@ export class ObjectiveTableComponent implements OnInit {
} }
} }
enum PentestColumns { enum ObjectiveColumns {
TEST_ID = 'testId', TEST_ID = 'testId',
TITLE = 'title', TITLE = 'title',
STATUS = 'status', STATUS = 'status',

View File

@ -5,7 +5,7 @@
<app-pentest-info [pentestInfo$] = pentest$></app-pentest-info> <app-pentest-info [pentestInfo$] = pentest$></app-pentest-info>
</nb-tab> </nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.findings' | translate }}" badgeText="{{currentNumberOfFindings$.getValue()}}" badgeStatus="danger"> <nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.findings' | translate }}" badgeText="{{currentNumberOfFindings$.getValue()}}" badgeStatus="danger">
<app-pentest-findings></app-pentest-findings> <app-pentest-findings [pentestInfo$] = pentest$></app-pentest-findings>
</nb-tab> </nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}" badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info"> <nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}" badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info">
<app-pentest-comments></app-pentest-comments> <app-pentest-comments></app-pentest-comments>

View File

@ -1 +1,83 @@
<p>pentest-findings works!</p> <div class="finding-table">
<table [nbTreeGrid]="dataSource">
<tr nbTreeGridHeaderRow *nbTreeGridHeaderRowDef="columns"></tr>
<tr nbTreeGridRow *nbTreeGridRowDef="let finding; columns: columns"
class="finding-cell"
fragment="{{finding.data['findingId']}}">
</tr>
<!-- Finding ID -->
<ng-container [nbTreeGridColumnDef]="columns[0]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.findingId' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
{{ finding.data['findingId'] || '-' }}
</td>
</ng-container>
<!-- Title -->
<ng-container [nbTreeGridColumnDef]="columns[1]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.title' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
{{ finding.data['title'] }}
</td>
</ng-container>
<!-- Impact -->
<ng-container [nbTreeGridColumnDef]="columns[2]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.impact' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
{{ finding.data['impact'] }}
</td>
</ng-container>
<!-- Severity -->
<ng-container [nbTreeGridColumnDef]="columns[3]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.severity' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
<app-severity-tag [currentSeverity]="finding.data['severity']"></app-severity-tag>
</td>
</ng-container>
<!-- Actions -->
<ng-container [nbTreeGridColumnDef]="columns[4]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-actions">
<button nbButton hero
status="info"
size="small"
shape="round"
class="add-finding-button"
(click)="onClickAddFinding()">
<fa-icon [icon]="fa.faPlus" class="new-finding-icon"></fa-icon>
{{'finding.add' | translate}}
</button>
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding" class="cell-actions">
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="1rem">
<button nbButton
status="primary"
size="small"
(click)="onClickEditFinding(finding)">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<button nbButton
status="danger"
size="small"
(click)="onClickDeleteFinding(finding)">
<fa-icon [icon]="fa.faTrash"></fa-icon>
</button>
</div>
</td>
</ng-container>
</table>
</div>
<div *ngIf="data.length === 0 && loading$.getValue() === false" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'finding.no.findings' | translate}}
</p>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,32 @@
@import '../../../../assets/@theme/styles/themes';
.finding-table {
// width: calc(78vw - 18%);
width: 90vw;
.finding-cell {
// Add style here
}
.finding-cell:hover {
// cursor: default;
background-color: nb-theme(color-basic-transparent-focus);
}
.cell-actions {
width: max-content;
max-width: 180px;
.add-finding-button {
.new-finding-icon {
padding-right: 0.5rem;
}
}
}
}
.error-text {
padding-top: 0.5rem;
font-size: 1.25rem;
font-weight: bold;
}

View File

@ -1,20 +1,92 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import { PentestFindingsComponent } from './pentest-findings.component'; import {PentestFindingsComponent} from './pentest-findings.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {CommonModule} from '@angular/common';
import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
findingsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentsIds: []
},
};
describe('PentestFindingsComponent', () => { describe('PentestFindingsComponent', () => {
let component: PentestFindingsComponent; let component: PentestFindingsComponent;
let fixture: ComponentFixture<PentestFindingsComponent>; let fixture: ComponentFixture<PentestFindingsComponent>;
let store: Store;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ PentestFindingsComponent ] declarations: [
PentestFindingsComponent,
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
NbButtonModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PentestFindingsComponent); fixture = TestBed.createComponent(PentestFindingsComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -1,5 +1,15 @@
import { Component, OnInit } from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {PentestService} from '@shared/services/pentest.service';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {tap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {Finding, FindingEntry, transformFindingsToObjectiveEntries} from '@shared/models/finding.model';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import * as FA from '@fortawesome/free-solid-svg-icons';
@UntilDestroy()
@Component({ @Component({
selector: 'app-pentest-findings', selector: 'app-pentest-findings',
templateUrl: './pentest-findings.component.html', templateUrl: './pentest-findings.component.html',
@ -7,9 +17,80 @@ import { Component, OnInit } from '@angular/core';
}) })
export class PentestFindingsComponent implements OnInit { export class PentestFindingsComponent implements OnInit {
constructor() { } @Input()
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
ngOnInit(): void { // HTML only
readonly fa = FA;
// findings$: BehaviorSubject<Finding[]> = new BehaviorSubject<Finding[]>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
columns: Array<FindingColumns> = [
FindingColumns.FINDING_ID, FindingColumns.TITLE, FindingColumns.IMPACT, FindingColumns.SEVERITY, FindingColumns.ACTIONS
];
dataSource: NbTreeGridDataSource<FindingEntry>;
data: FindingEntry[] = [];
getters: NbGetters<FindingEntry, FindingEntry> = {
dataGetter: (node: FindingEntry) => node,
childrenGetter: (node: FindingEntry) => node.childEntries || undefined,
expandedGetter: (node: FindingEntry) => !!node.expanded,
};
constructor(private readonly pentestService: PentestService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
private notificationService: NotificationService) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
} }
ngOnInit(): void {
console.warn('Selected Pentest: ', this.pentestInfo$.getValue());
this.loadFindingsData();
}
loadFindingsData(): void {
this.pentestService.getFindingsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe(
untilDestroyed(this),
tap(() => this.loading$.next(true))
)
.subscribe({
next: (findings: Finding[]) => {
this.data = transformFindingsToObjectiveEntries(findings);
this.dataSource.setData(this.data, this.getters);
this.loading$.next(false);
},
error: err => {
console.log(err);
this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE);
this.loading$.next(false);
}
});
}
onClickAddFinding(): void {
console.info('Coming soon..');
}
onClickEditFinding(finding): void {
console.info('Coming soon..');
}
onClickDeleteFinding(finding): void{
console.info('Coming soon..');
}
// HTML only
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
}
enum FindingColumns {
FINDING_ID = 'findingId',
TITLE = 'title',
IMPACT = 'impact',
SEVERITY = 'severity',
ACTIONS = 'actions'
} }

View File

@ -5,5 +5,5 @@
<p class="description"> <p class="description">
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }} {{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
</p> </p>
<!--ToDo: Add tooling hints after description--> <!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
</div> </div>

View File

@ -1,6 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import { PentestInfoComponent } from './pentest-info.component'; import {PentestInfoComponent} from './pentest-info.component';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
describe('PentestInfoComponent', () => { describe('PentestInfoComponent', () => {
let component: PentestInfoComponent; let component: PentestInfoComponent;
@ -8,7 +20,24 @@ describe('PentestInfoComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ PentestInfoComponent ] declarations: [
PentestInfoComponent
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
}) })
.compileComponents(); .compileComponents();
}); });
@ -16,6 +45,15 @@ describe('PentestInfoComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PentestInfoComponent); fixture = TestBed.createComponent(PentestInfoComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.pentestInfo$.next({
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
findingsIds: [],
commentsIds: []
});
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -17,7 +17,6 @@ export class PentestInfoComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
console.warn('Selected Pentest: ', this.pentestInfo$.getValue());
} }
getPentestHeaderForObjective(refNumber: string): string { getPentestHeaderForObjective(refNumber: string): string {

View File

@ -2,7 +2,7 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {PentestComponent} from './pentest.component'; import {PentestComponent} from './pentest.component';
import {NbButtonModule, NbCardModule, NbLayoutModule, NbTabsetModule} from '@nebular/theme'; import {NbButtonModule, NbCardModule, NbLayoutModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
import { PentestHeaderComponent } from './pentest-header/pentest-header.component'; import { PentestHeaderComponent } from './pentest-header/pentest-header.component';
import { PentestContentComponent } from './pentest-content/pentest-content.component'; import { PentestContentComponent } from './pentest-content/pentest-content.component';
import {FlexLayoutModule} from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
@ -12,6 +12,10 @@ import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
import { PentestInfoComponent } from './pentest-content/pentest-info/pentest-info.component'; import { PentestInfoComponent } from './pentest-content/pentest-info/pentest-info.component';
import { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.component'; import { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.component';
import { PentestCommentsComponent } from './pentest-content/pentest-comments/pentest-comments.component'; import { PentestCommentsComponent } from './pentest-content/pentest-comments/pentest-comments.component';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {ProjectOverviewModule} from '../project-overview';
import {CommonAppModule} from '../common-app.module';
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -35,7 +39,10 @@ import { PentestCommentsComponent } from './pentest-content/pentest-comments/pen
TranslateModule, TranslateModule,
NbButtonModule, NbButtonModule,
StatusTagModule, StatusTagModule,
NbTabsetModule NbTabsetModule,
NbTreeGridModule,
CommonAppModule,
SeverityTagModule
] ]
}) })
export class PentestModule { export class PentestModule {

View File

@ -1,5 +1,5 @@
<div fxLayout="row" fxLayoutGap="2rem"> <div fxLayout="row" fxLayoutGap="2rem">
<div *ngFor="let project of projects | async"> <div *ngFor="let project of projects$ | async">
<nb-card class="project-card" accent="success"> <nb-card class="project-card" accent="success">
<nb-card-header fxLayoutAlign="start center" <nb-card-header fxLayoutAlign="start center"
routerLink="id" routerLink="id"
@ -68,15 +68,15 @@
</div> </div>
</div> </div>
<div *ngIf="projects.getValue().length === 0 && loading$.getValue() === false" fxLayout="row" fxLayoutAlign="center center"> <div *ngIf="projects$.getValue().length === 0 && loading$.getValue() === false" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text"> <p class="error-text">
{{'project.overview.no.projects' | translate}} {{'project.overview.no.projects' | translate}}
</p> </p>
</div> </div>
<div fxLayoutAlign="end end"> <div fxLayoutAlign="end end">
<button nbButton <button nbButton hero
status="primary" status="info"
size="large" size="large"
shape="round" shape="round"
class="add-project-button" class="add-project-button"
@ -87,5 +87,3 @@
</div> </div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner> <app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -10,7 +10,7 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/project.service';
import {HttpLoaderFactory} from '../common-app.module'; import {HttpLoaderFactory} from '../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store'; import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state'; import {SessionState} from '@shared/stores/session-state/session-state';

View File

@ -21,7 +21,7 @@ export class ProjectOverviewComponent implements OnInit {
readonly fa = FA; readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]); projects$: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
constructor( constructor(
private readonly projectService: ProjectService, private readonly projectService: ProjectService,
@ -42,7 +42,7 @@ export class ProjectOverviewComponent implements OnInit {
) )
.subscribe({ .subscribe({
next: (projects: Project[]) => { next: (projects: Project[]) => {
this.projects.next(projects); this.projects$.next(projects);
this.loading$.next(false); this.loading$.next(false);
}, },
error: err => { error: err => {

View File

@ -8,13 +8,12 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe'; import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe';
import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module'; import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {CommonAppModule} from '../common-app.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
ProjectOverviewComponent, ProjectOverviewComponent,
DateTimeFormatPipe, DateTimeFormatPipe,
LoadingSpinnerComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -26,10 +25,8 @@ import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-s
FlexLayoutModule, FlexLayoutModule,
FontAwesomeModule, FontAwesomeModule,
TranslateModule, TranslateModule,
ProjectDialogModule ProjectDialogModule,
], CommonAppModule
exports: [
LoadingSpinnerComponent
] ]
}) })
export class ProjectOverviewModule { export class ProjectOverviewModule {

View File

@ -88,6 +88,20 @@
"BUSINESS_LOGIC_TESTING": "Business-Logik-Testing", "BUSINESS_LOGIC_TESTING": "Business-Logik-Testing",
"CLIENT_SIDE_TESTING": "Clientseitiges-Testing" "CLIENT_SIDE_TESTING": "Clientseitiges-Testing"
}, },
"finding": {
"findingId": "Fund Id",
"title": "Title",
"impact": "Auswirkung",
"severity": "Schwere",
"add": "Fund hinzufügen",
"no.findings": "Keine Funde verfügbar"
},
"severities": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch",
"critical": "Kritisch"
},
"pentest": { "pentest": {
"testId": "Nr.", "testId": "Nr.",
"title": "Titel", "title": "Titel",

View File

@ -88,6 +88,20 @@
"BUSINESS_LOGIC_TESTING": "Business Logic Testing", "BUSINESS_LOGIC_TESTING": "Business Logic Testing",
"CLIENT_SIDE_TESTING": "Client Side Testing" "CLIENT_SIDE_TESTING": "Client Side Testing"
}, },
"finding": {
"findingId": "Finding Id",
"title": "Title",
"impact": "Impact",
"severity": "Severity",
"add": "Add finding",
"no.findings": "No findings available"
},
"severities": {
"low": "Low",
"medium": "Medium",
"high": "High",
"critical": "Critical"
},
"pentest": { "pentest": {
"testId": "No.", "testId": "No.",
"title": "Title", "title": "Title",

View File

@ -0,0 +1,57 @@
import {v4 as UUID} from 'uuid';
import {Severity} from '@shared/models/severity.enum';
export class Finding {
id?: string;
title: string;
description?: string;
impact: string;
severity: Severity;
affectedUrls?: Array<string>;
reproduction?: string;
mitigation?: string;
constructor(title: string,
description: string,
impact: string,
severity: Severity,
reproduction: string,
id?: string,
affectedUrls?: Array<string>,
mitigation?: string) {
this.id = id ? id : UUID();
this.title = title;
this.description = description;
this.impact = impact;
this.severity = severity;
this.affectedUrls = affectedUrls ? affectedUrls : null;
this.reproduction = reproduction;
this.mitigation = mitigation ? mitigation : null;
}
}
export interface FindingEntry {
findingId: string;
title: string;
impact: string;
severity: Severity;
kind?: string;
childEntries?: [];
expanded?: boolean;
}
export function transformFindingsToObjectiveEntries(findings: Finding[]): FindingEntry[] {
const findingEntries: FindingEntry[] = [];
findings.forEach((value: Finding) => {
findingEntries.push({
findingId: value.id,
title: value.title,
impact: value.impact,
severity: value.severity,
kind: 'cell',
childEntries: null,
expanded: false
} as FindingEntry);
});
return findingEntries;
}

View File

@ -26,26 +26,26 @@ export class Pentest {
} }
} }
export interface PentestEntry { export interface ObjectiveEntry {
refNumber: string; refNumber: string;
status: string; status: string;
findings?: number; findings?: number;
kind?: string; kind?: string;
childEntries?: PentestEntry[]; childEntries?: ObjectiveEntry[];
expanded?: boolean; expanded?: boolean;
} }
export function transformPentestsToEntries(pentests: Pentest[]): PentestEntry[] { export function transformPentestsToObjectiveEntries(pentests: Pentest[]): ObjectiveEntry[] {
const pentestEntries: PentestEntry[] = []; const objectiveEntries: ObjectiveEntry[] = [];
pentests.forEach((value: Pentest) => { pentests.forEach((value: Pentest) => {
pentestEntries.push({ objectiveEntries.push({
refNumber: value.refNumber, refNumber: value.refNumber,
status: value.status, status: value.status,
findings: value.findingsIds ? value.findingsIds.length : 0, findings: value.findingsIds ? value.findingsIds.length : 0,
kind: value.childEntries ? 'dir' : 'cell', kind: value.childEntries ? 'dir' : 'cell',
childEntries: value.childEntries ? value.childEntries : null, childEntries: value.childEntries ? value.childEntries : null,
expanded: !!value.childEntries expanded: !!value.childEntries
} as PentestEntry); } as ObjectiveEntry);
}); });
return pentestEntries; return objectiveEntries;
} }

View File

@ -0,0 +1,6 @@
export enum Severity {
LOW,
MEDIUM,
HIGH,
CRITICAL
}

View File

@ -8,6 +8,8 @@ import {Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {catchError, map, switchMap} from 'rxjs/operators'; import {catchError, map, switchMap} from 'rxjs/operators';
import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp-pentests-for-category.function'; import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp-pentests-for-category.function';
import {Finding} from '@shared/models/finding.model';
import {Severity} from '@shared/models/severity.enum';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -50,4 +52,44 @@ export class PentestService {
const queryParams = new HttpParams().append('projectId', projectId).append('category', Category[category]); const queryParams = new HttpParams().append('projectId', projectId).append('category', Category[category]);
return this.http.get<Pentest[]>(`${this.apiBaseURL}`, {params: queryParams}); return this.http.get<Pentest[]>(`${this.apiBaseURL}`, {params: queryParams});
} }
/**
* Get Findings for Pentest Id
* @param pentestId the id of the project
*/
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
console.warn('Findings for:', pentestId);
if (pentestId) {
return this.http.get<Finding[]>(`${this.apiBaseURL}/${pentestId}/findings`);
} else {
// return of([]);
// Todo: Remove mocked Findings
return of([
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
title: 'This is a lit test finding ma brother',
impact: 'fucked up a lot man. better fix it',
severity: Severity.LOW,
},
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
title: 'This is a lit test finding ma brother',
impact: 'fucked up a lot man. better fix it',
severity: Severity.MEDIUM,
},
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
title: 'This is a lit test finding ma brother',
impact: 'fucked up a lot man. better fix it',
severity: Severity.HIGH,
},
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
title: 'This is a lit test finding ma brother',
impact: 'fucked up a lot man. better fix it',
severity: Severity.CRITICAL,
}
]);
}
}
} }

View File

@ -0,0 +1,14 @@
<ng-container [ngSwitch]="currentSeverity">
<nb-tag-list>
<nb-tag *ngSwitchCase="severity.LOW" status="basic" appearance="filled"
text="{{getTranslationKey() | translate}}"></nb-tag>
<nb-tag *ngSwitchCase="severity.MEDIUM" status="info" appearance="filled"
text=" {{getTranslationKey() | translate}}"></nb-tag>
<nb-tag *ngSwitchCase="severity.HIGH" status="warning" appearance="filled"
text="{{getTranslationKey() | translate}}"></nb-tag>
<nb-tag *ngSwitchCase="severity.CRITICAL" status="danger" appearance="filled"
text="{{getTranslationKey() | translate}}"></nb-tag>
<nb-tag *ngSwitchDefault status="basic" appearance="filled"
text="{{getTranslationKey() | translate}}"></nb-tag>
</nb-tag-list>
</ng-container>

View File

@ -0,0 +1,45 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SeverityTagComponent} from './severity-tag.component';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NbCardModule, NbTagModule} from '@nebular/theme';
import {MockModule} from 'ng-mocks';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient} from '@angular/common/http';
describe('SeverityTagComponent', () => {
let component: SeverityTagComponent;
let fixture: ComponentFixture<SeverityTagComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
SeverityTagComponent
],
imports: [
HttpClientTestingModule,
NbCardModule,
MockModule(NbTagModule),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SeverityTagComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
import {Severity} from '@shared/models/severity.enum';
@Component({
selector: 'app-severity-tag',
templateUrl: './severity-tag.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeverityTagComponent implements OnInit {
@Input() currentSeverity: Severity = Severity.LOW;
// HTML only
severity = Severity;
readonly severityTexts: Array<SeverityText> = [
{value: Severity.LOW, translationText: 'severities.low'},
{value: Severity.MEDIUM, translationText: 'severities.medium'},
{value: Severity.HIGH, translationText: 'severities.high'},
{value: Severity.CRITICAL, translationText: 'severities.critical'}
];
constructor() { }
ngOnInit(): void {
}
getTranslationKey(): string {
const index = this.severityTexts.findIndex(statusText => statusText.value === this.currentSeverity);
return this.severityTexts[index].translationText;
}
}
interface SeverityText {
value: Severity;
translationText: string;
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {SeverityTagComponent} from '@shared/widgets/severity-tag/severity-tag.component';
import {NbTagModule} from '@nebular/theme';
import {TranslateModule} from '@ngx-translate/core';
@NgModule({
declarations: [
SeverityTagComponent
],
exports: [
SeverityTagComponent
],
imports: [
CommonModule,
NbTagModule,
TranslateModule
]
})
export class SeverityTagModule { }

View File

@ -4,8 +4,7 @@ import {PentestStatus} from '@shared/models/pentest-status.model';
@Component({ @Component({
selector: 'app-status-tag', selector: 'app-status-tag',
templateUrl: './status-tag.component.html', templateUrl: './status-tag.component.html',
styleUrls: ['./status-tag.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class StatusTagComponent implements OnInit { export class StatusTagComponent implements OnInit {
@Input() currentStatus: PentestStatus = PentestStatus.NOT_STARTED; @Input() currentStatus: PentestStatus = PentestStatus.NOT_STARTED;
@ -29,7 +28,6 @@ export class StatusTagComponent implements OnInit {
const index = this.statusTexts.findIndex(statusText => statusText.value === this.currentStatus); const index = this.statusTexts.findIndex(statusText => statusText.value === this.currentStatus);
return this.statusTexts[index].translationText; return this.statusTexts[index].translationText;
} }
} }
interface StatusText { interface StatusText {

View File

@ -0,0 +1,16 @@
package com.securityc4po.api.pentest
import org.springframework.data.mongodb.core.index.Indexed
import java.util.*
data class Finding (
@Indexed(background = true, unique = true)
val id: String = UUID.randomUUID().toString(),
val title: String,
val description: String,
val impact: String,
val severity: Severity,
val affectedUrls: List<String>? = emptyList(),
val reproduction: String,
val mitigation: String
)

View File

@ -0,0 +1,8 @@
package com.securityc4po.api.pentest
enum class Severity {
LOW,
MEDIUM,
HIGH,
CRITICAL
}