feat: As a user I want to have an findings overview
This commit is contained in:
parent
6764583481
commit
747cade495
|
@ -9,13 +9,16 @@ import {MomentModule} from 'ngx-moment';
|
|||
import {NotificationService} from '../shared/services/notification.service';
|
||||
import {NbToastrModule} from '@nebular/theme';
|
||||
import {ThemeModule} from '../assets/@theme/theme.module';
|
||||
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
declarations: [
|
||||
LoadingSpinnerComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbToastrModule, // used for notification service
|
||||
|
@ -37,6 +40,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
|||
NotificationService
|
||||
],
|
||||
exports: [
|
||||
LoadingSpinnerComponent,
|
||||
// modules
|
||||
MomentModule
|
||||
]
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<nb-actions size="medium">
|
||||
<nb-action>
|
||||
<button nbButton hero
|
||||
status="primary"
|
||||
status="info"
|
||||
shape="round"
|
||||
(click)="onClickExportPentest()">
|
||||
<fa-icon [icon]="fa.faFileExport"
|
||||
|
|
|
@ -21,6 +21,7 @@ import {FormsModule} from '@angular/forms';
|
|||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
|
||||
import {CommonAppModule} from '../common-app.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -48,7 +49,8 @@ import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-s
|
|||
NbListModule,
|
||||
FontAwesomeModule,
|
||||
FlexLayoutModule,
|
||||
NbActionsModule
|
||||
NbActionsModule,
|
||||
CommonAppModule
|
||||
],
|
||||
exports: [
|
||||
ObjectiveHeaderComponent,
|
||||
|
|
|
@ -54,5 +54,5 @@
|
|||
</table>
|
||||
</nb-card>
|
||||
|
||||
<!--ToDo: Add loading spinner after routing fix to avoid circular dependency issues
|
||||
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>-->
|
||||
<!--ToDo: Add loading spinner after routing fix to avoid circular dependency issues -->
|
||||
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
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 {Store} from '@ngxs/store';
|
||||
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 {
|
||||
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
columns: Array<PentestColumns> = [PentestColumns.TEST_ID, PentestColumns.TITLE, PentestColumns.STATUS, PentestColumns.FINDINGS];
|
||||
dataSource: NbTreeGridDataSource<PentestEntry>;
|
||||
columns: Array<ObjectiveColumns> = [ObjectiveColumns.TEST_ID, ObjectiveColumns.TITLE, ObjectiveColumns.STATUS, ObjectiveColumns.FINDINGS];
|
||||
dataSource: NbTreeGridDataSource<ObjectiveEntry>;
|
||||
|
||||
private data: PentestEntry[] = [];
|
||||
private data: ObjectiveEntry[] = [];
|
||||
|
||||
getters: NbGetters<PentestEntry, PentestEntry> = {
|
||||
dataGetter: (node: PentestEntry) => node,
|
||||
childrenGetter: (node: PentestEntry) => node.childEntries || undefined,
|
||||
expandedGetter: (node: PentestEntry) => !!node.expanded,
|
||||
getters: NbGetters<ObjectiveEntry, ObjectiveEntry> = {
|
||||
dataGetter: (node: ObjectiveEntry) => node,
|
||||
childrenGetter: (node: ObjectiveEntry) => node.childEntries || undefined,
|
||||
expandedGetter: (node: ObjectiveEntry) => !!node.expanded,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
private pentestService: PentestService,
|
||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<PentestEntry>,
|
||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<ObjectiveEntry>,
|
||||
private readonly router: Router
|
||||
) {
|
||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||
|
@ -53,7 +53,7 @@ export class ObjectiveTableComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (pentests: Pentest[]) => {
|
||||
this.data = transformPentestsToEntries(pentests);
|
||||
this.data = transformPentestsToObjectiveEntries(pentests);
|
||||
this.dataSource.setData(this.data, this.getters);
|
||||
this.loading$.next(false);
|
||||
},
|
||||
|
@ -88,7 +88,7 @@ export class ObjectiveTableComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
enum PentestColumns {
|
||||
enum ObjectiveColumns {
|
||||
TEST_ID = 'testId',
|
||||
TITLE = 'title',
|
||||
STATUS = 'status',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<app-pentest-info [pentestInfo$] = pentest$></app-pentest-info>
|
||||
</nb-tab>
|
||||
<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 class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}" badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info">
|
||||
<app-pentest-comments></app-pentest-comments>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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', () => {
|
||||
let component: PentestFindingsComponent;
|
||||
let fixture: ComponentFixture<PentestFindingsComponent>;
|
||||
let store: Store;
|
||||
|
||||
beforeEach(async () => {
|
||||
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();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PentestFindingsComponent);
|
||||
store = TestBed.inject(Store);
|
||||
store.reset({
|
||||
...store.snapshot(),
|
||||
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
|
||||
});
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
selector: 'app-pentest-findings',
|
||||
templateUrl: './pentest-findings.component.html',
|
||||
|
@ -7,9 +17,80 @@ import { Component, OnInit } from '@angular/core';
|
|||
})
|
||||
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'
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
<p class="description">
|
||||
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
|
||||
</p>
|
||||
<!--ToDo: Add tooling hints after description-->
|
||||
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
|
||||
</div>
|
||||
|
|
|
@ -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', () => {
|
||||
let component: PentestInfoComponent;
|
||||
|
@ -8,7 +20,24 @@ describe('PentestInfoComponent', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
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();
|
||||
});
|
||||
|
@ -16,6 +45,15 @@ describe('PentestInfoComponent', () => {
|
|||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PentestInfoComponent);
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ export class PentestInfoComponent implements OnInit {
|
|||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
console.warn('Selected Pentest: ', this.pentestInfo$.getValue());
|
||||
}
|
||||
|
||||
getPentestHeaderForObjective(refNumber: string): string {
|
||||
|
|
|
@ -2,7 +2,7 @@ import {NgModule} from '@angular/core';
|
|||
import {CommonModule} from '@angular/common';
|
||||
import {RouterModule} from '@angular/router';
|
||||
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 { PentestContentComponent } from './pentest-content/pentest-content.component';
|
||||
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 { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.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({
|
||||
declarations: [
|
||||
|
@ -35,7 +39,10 @@ import { PentestCommentsComponent } from './pentest-content/pentest-comments/pen
|
|||
TranslateModule,
|
||||
NbButtonModule,
|
||||
StatusTagModule,
|
||||
NbTabsetModule
|
||||
NbTabsetModule,
|
||||
NbTreeGridModule,
|
||||
CommonAppModule,
|
||||
SeverityTagModule
|
||||
]
|
||||
})
|
||||
export class PentestModule {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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-header fxLayoutAlign="start center"
|
||||
routerLink="id"
|
||||
|
@ -68,15 +68,15 @@
|
|||
</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">
|
||||
{{'project.overview.no.projects' | translate}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div fxLayoutAlign="end end">
|
||||
<button nbButton
|
||||
status="primary"
|
||||
<button nbButton hero
|
||||
status="info"
|
||||
size="large"
|
||||
shape="round"
|
||||
class="add-project-button"
|
||||
|
@ -87,5 +87,3 @@
|
|||
</div>
|
||||
|
||||
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
|||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {ProjectService} from '@shared/services/project.service';
|
||||
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 {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||
|
|
|
@ -21,7 +21,7 @@ export class ProjectOverviewComponent implements OnInit {
|
|||
readonly fa = FA;
|
||||
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
|
||||
projects$: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
|
||||
|
||||
constructor(
|
||||
private readonly projectService: ProjectService,
|
||||
|
@ -42,7 +42,7 @@ export class ProjectOverviewComponent implements OnInit {
|
|||
)
|
||||
.subscribe({
|
||||
next: (projects: Project[]) => {
|
||||
this.projects.next(projects);
|
||||
this.projects$.next(projects);
|
||||
this.loading$.next(false);
|
||||
},
|
||||
error: err => {
|
||||
|
|
|
@ -8,13 +8,12 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
|||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe';
|
||||
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({
|
||||
declarations: [
|
||||
ProjectOverviewComponent,
|
||||
DateTimeFormatPipe,
|
||||
LoadingSpinnerComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -26,10 +25,8 @@ import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-s
|
|||
FlexLayoutModule,
|
||||
FontAwesomeModule,
|
||||
TranslateModule,
|
||||
ProjectDialogModule
|
||||
],
|
||||
exports: [
|
||||
LoadingSpinnerComponent
|
||||
ProjectDialogModule,
|
||||
CommonAppModule
|
||||
]
|
||||
})
|
||||
export class ProjectOverviewModule {
|
||||
|
|
|
@ -88,6 +88,20 @@
|
|||
"BUSINESS_LOGIC_TESTING": "Business-Logik-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": {
|
||||
"testId": "Nr.",
|
||||
"title": "Titel",
|
||||
|
|
|
@ -88,6 +88,20 @@
|
|||
"BUSINESS_LOGIC_TESTING": "Business Logic 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": {
|
||||
"testId": "No.",
|
||||
"title": "Title",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -26,26 +26,26 @@ export class Pentest {
|
|||
}
|
||||
}
|
||||
|
||||
export interface PentestEntry {
|
||||
export interface ObjectiveEntry {
|
||||
refNumber: string;
|
||||
status: string;
|
||||
findings?: number;
|
||||
kind?: string;
|
||||
childEntries?: PentestEntry[];
|
||||
childEntries?: ObjectiveEntry[];
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export function transformPentestsToEntries(pentests: Pentest[]): PentestEntry[] {
|
||||
const pentestEntries: PentestEntry[] = [];
|
||||
export function transformPentestsToObjectiveEntries(pentests: Pentest[]): ObjectiveEntry[] {
|
||||
const objectiveEntries: ObjectiveEntry[] = [];
|
||||
pentests.forEach((value: Pentest) => {
|
||||
pentestEntries.push({
|
||||
objectiveEntries.push({
|
||||
refNumber: value.refNumber,
|
||||
status: value.status,
|
||||
findings: value.findingsIds ? value.findingsIds.length : 0,
|
||||
kind: value.childEntries ? 'dir' : 'cell',
|
||||
childEntries: value.childEntries ? value.childEntries : null,
|
||||
expanded: !!value.childEntries
|
||||
} as PentestEntry);
|
||||
} as ObjectiveEntry);
|
||||
});
|
||||
return pentestEntries;
|
||||
return objectiveEntries;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export enum Severity {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
|
@ -8,6 +8,8 @@ import {Store} from '@ngxs/store';
|
|||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||
import {catchError, map, switchMap} from 'rxjs/operators';
|
||||
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({
|
||||
providedIn: 'root'
|
||||
|
@ -50,4 +52,44 @@ export class PentestService {
|
|||
const queryParams = new HttpParams().append('projectId', projectId).append('category', Category[category]);
|
||||
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,
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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 { }
|
|
@ -4,8 +4,7 @@ import {PentestStatus} from '@shared/models/pentest-status.model';
|
|||
@Component({
|
||||
selector: 'app-status-tag',
|
||||
templateUrl: './status-tag.component.html',
|
||||
styleUrls: ['./status-tag.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StatusTagComponent implements OnInit {
|
||||
@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);
|
||||
return this.statusTexts[index].translationText;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface StatusText {
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package com.securityc4po.api.pentest
|
||||
|
||||
enum class Severity {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
Loading…
Reference in New Issue