feat: As a user I want to add a finding via dialog
This commit is contained in:
parent
5d89467c1e
commit
f658073bcf
|
@ -11338,9 +11338,9 @@
|
||||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||||
},
|
},
|
||||||
"moment-timezone": {
|
"moment-timezone": {
|
||||||
"version": "0.5.34",
|
"version": "0.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.38.tgz",
|
||||||
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
|
"integrity": "sha512-nMIrzGah4+oYZPflDvLZUgoVUO4fvAqHstvG3xAUnMolWncuAiLDWNnJZj6EwJGMGfb1ZcuTFE6GI3hNOVWI/Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"moment": ">= 2.9.0"
|
"moment": ">= 2.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
import {ThemeModule} from '@assets/@theme/theme.module';
|
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
import {Category} from '@shared/models/category.model';
|
import {Category} from '@shared/models/category.model';
|
||||||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||||
|
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||||
|
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
|
||||||
|
|
||||||
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||||
selectedProject: {
|
selectedProject: {
|
||||||
|
@ -74,7 +76,8 @@ describe('PentestFindingsComponent', () => {
|
||||||
NgxsModule.forRoot([ProjectState])
|
NgxsModule.forRoot([ProjectState])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()}
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
|
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import {Component, Input, OnInit} from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import {PentestService} from '@shared/services/pentest.service';
|
import {PentestService} from '@shared/services/pentest.service';
|
||||||
import {BehaviorSubject, Observable, of} from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
import {Pentest} from '@shared/models/pentest.model';
|
import {Pentest} from '@shared/models/pentest.model';
|
||||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||||
import {filter, tap} from 'rxjs/operators';
|
import {filter, mergeMap, tap} from 'rxjs/operators';
|
||||||
import {NotificationService, PopupType} from '@shared/services/notification.service';
|
import {NotificationService, PopupType} from '@shared/services/notification.service';
|
||||||
import {Finding, FindingEntry, transformFindingsToObjectiveEntries} from '@shared/models/finding.model';
|
import {Finding, FindingDialogBody, FindingEntry, transformFindingsToObjectiveEntries} from '@shared/models/finding.model';
|
||||||
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
||||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
||||||
|
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||||
|
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
||||||
|
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -23,7 +25,6 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
|
|
||||||
// HTML only
|
// HTML only
|
||||||
readonly fa = FA;
|
readonly fa = FA;
|
||||||
// findings$: BehaviorSubject<Finding[]> = new BehaviorSubject<Finding[]>(null);
|
|
||||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
columns: Array<FindingColumns> = [
|
columns: Array<FindingColumns> = [
|
||||||
|
@ -41,7 +42,8 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
|
|
||||||
constructor(private readonly pentestService: PentestService,
|
constructor(private readonly pentestService: PentestService,
|
||||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
|
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
|
||||||
private notificationService: NotificationService) {
|
private notificationService: NotificationService,
|
||||||
|
private findingDialogService: FindingDialogService) {
|
||||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +73,39 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickAddFinding(): void {
|
onClickAddFinding(): void {
|
||||||
console.info('Coming soon..');
|
this.findingDialogService.openFindingDialog(
|
||||||
|
FindingDialogComponent,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
closeOnEsc: false,
|
||||||
|
hasScroll: false,
|
||||||
|
autoFocus: false,
|
||||||
|
closeOnBackdropClick: false
|
||||||
|
}
|
||||||
|
).pipe(
|
||||||
|
filter(value => !!value),
|
||||||
|
tap((value) => console.warn('FindingDialogBody: ', value)),
|
||||||
|
mergeMap((value: FindingDialogBody) =>
|
||||||
|
this.pentestService.saveFinding(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', value)
|
||||||
|
),
|
||||||
|
untilDestroyed(this)
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadFindingsData();
|
||||||
|
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
console.error(error);
|
||||||
|
this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickEditFinding(finding): void {
|
onClickEditFinding(finding): void {
|
||||||
console.info('Coming soon..');
|
console.info('Coming soon..');
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickDeleteFinding(finding): void{
|
onClickDeleteFinding(finding): void {
|
||||||
console.info('Coming soon..');
|
console.info('Coming soon..');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,9 @@ 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 {CommonAppModule} from '../common-app.module';
|
||||||
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
|
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
|
||||||
|
import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -42,7 +41,8 @@ import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.modul
|
||||||
NbTabsetModule,
|
NbTabsetModule,
|
||||||
NbTreeGridModule,
|
NbTreeGridModule,
|
||||||
CommonAppModule,
|
CommonAppModule,
|
||||||
SeverityTagModule
|
SeverityTagModule,
|
||||||
|
FindingDialogModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PentestModule {
|
export class PentestModule {
|
||||||
|
|
|
@ -90,11 +90,43 @@
|
||||||
},
|
},
|
||||||
"finding": {
|
"finding": {
|
||||||
"findingId": "Fund Id",
|
"findingId": "Fund Id",
|
||||||
"title": "Titel",
|
|
||||||
"impact": "Auswirkung",
|
|
||||||
"severity": "Schwere",
|
"severity": "Schwere",
|
||||||
|
"title": "Titel",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"impact": "Auswirkung",
|
||||||
|
"affectedUrls": "Betroffene URL's",
|
||||||
|
"reproduction": "Reproduktion",
|
||||||
|
"mitigation": "Minderung",
|
||||||
"add": "Fund hinzufügen",
|
"add": "Fund hinzufügen",
|
||||||
|
"add.url": "Betroffene URL hinzufügen",
|
||||||
|
"affectedUrls.placeholder": "Betroffene URL hier eingeben..",
|
||||||
|
"create": {
|
||||||
|
"header": "Neuen Fund erstellen"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"header": "Fund editieren"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Fund löschen",
|
||||||
|
"key": "Möchten Sie den Fund \"{{name}}\" unwiderruflich löschen?"
|
||||||
|
},
|
||||||
|
"severity.label": "Schwere des Funds",
|
||||||
|
"title.label": "Fundtitel",
|
||||||
|
"description.label": "Beschreibung des Funds",
|
||||||
|
"impact.label": "Auswirkung auf Anwendung",
|
||||||
|
"affectedUrls.label": "Betroffene URL's",
|
||||||
|
"reproduction.label": "Reproduktionsschritte",
|
||||||
|
"mitigation.label": "Minderungsvorschlag",
|
||||||
"no.findings": "Keine Funde verfügbar",
|
"no.findings": "Keine Funde verfügbar",
|
||||||
|
"validationMessage": {
|
||||||
|
"titleRequired": "Titel ist erforderlich.",
|
||||||
|
"severityRequired": "Schwere ist erforderlich.",
|
||||||
|
"descriptionRequired": "Beschreibung ist erforderlich.",
|
||||||
|
"impactRequired": "Auswirkung ist erforderlich.",
|
||||||
|
"affectedUrlsRequired": "Betroffene URL's erforderlich.",
|
||||||
|
"reproductionRequired": "Reproduktionschritt(e) sind erforderlich.",
|
||||||
|
"mitigationRequired": "Minderungsvorschlag ist erforderlich."
|
||||||
|
},
|
||||||
"popup": {
|
"popup": {
|
||||||
"not.found": "Keine Funde gefunden",
|
"not.found": "Keine Funde gefunden",
|
||||||
"save.success": "Fund erfolgreich gespeichert",
|
"save.success": "Fund erfolgreich gespeichert",
|
||||||
|
|
|
@ -92,9 +92,41 @@
|
||||||
"findingId": "Finding Id",
|
"findingId": "Finding Id",
|
||||||
"severity": "Severity",
|
"severity": "Severity",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
|
"description": "Description",
|
||||||
"impact": "Impact",
|
"impact": "Impact",
|
||||||
|
"affectedUrls": "Affected URL's",
|
||||||
|
"reproduction": "Reproduction",
|
||||||
|
"mitigation": "Mitigation",
|
||||||
"add": "Add finding",
|
"add": "Add finding",
|
||||||
|
"add.url": "Add affected Url",
|
||||||
|
"affectedUrls.placeholder": "Enter affected URL here..",
|
||||||
|
"create": {
|
||||||
|
"header": "Create New Finding"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"header": "Edit Finding"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Delete Finding",
|
||||||
|
"key": "Do you want to permanently delete the finding \"{{name}}\"?"
|
||||||
|
},
|
||||||
|
"severity.label": "Severity of Finding",
|
||||||
|
"title.label": "Finding Title",
|
||||||
|
"description.label": "Description of Finding",
|
||||||
|
"impact.label": "Impacted Applicationparts",
|
||||||
|
"affectedUrls.label": "Affected URL's",
|
||||||
|
"reproduction.label": "Reproductionsteps",
|
||||||
|
"mitigation.label": "Suggest Mitigation",
|
||||||
"no.findings": "No findings available",
|
"no.findings": "No findings available",
|
||||||
|
"validationMessage": {
|
||||||
|
"titleRequired": "Title is required.",
|
||||||
|
"severityRequired": "Severity is required.",
|
||||||
|
"descriptionRequired": "Description is required.",
|
||||||
|
"impactRequired": "Impact is required.",
|
||||||
|
"affectedUrlsRequired": "Affected Url's required.",
|
||||||
|
"reproductionRequired": "Reproductionstep(s) are required.",
|
||||||
|
"mitigationRequired": "Mitigation is required."
|
||||||
|
},
|
||||||
"popup": {
|
"popup": {
|
||||||
"not.found": "No finding found",
|
"not.found": "No finding found",
|
||||||
"save.success": "Finding saved successfully",
|
"save.success": "Finding saved successfully",
|
||||||
|
@ -116,7 +148,7 @@
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"relatedFindings": "Related Findings",
|
"relatedFindings": "Related Findings",
|
||||||
"add": "Add comment",
|
"add": "Add Comment",
|
||||||
"no.comments": "No comments available",
|
"no.comments": "No comments available",
|
||||||
"no.relatedFindings": "Not related to finding",
|
"no.relatedFindings": "Not related to finding",
|
||||||
"popup": {
|
"popup": {
|
||||||
|
|
|
@ -5,10 +5,10 @@ export class Finding {
|
||||||
id?: string;
|
id?: string;
|
||||||
severity: Severity;
|
severity: Severity;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description: string;
|
||||||
impact: string;
|
impact: string;
|
||||||
affectedUrls?: Array<string>;
|
affectedUrls?: Array<string>;
|
||||||
reproduction?: string;
|
reproduction: string;
|
||||||
mitigation?: string;
|
mitigation?: string;
|
||||||
|
|
||||||
constructor(title: string,
|
constructor(title: string,
|
||||||
|
@ -55,3 +55,13 @@ export function transformFindingsToObjectiveEntries(findings: Finding[]): Findin
|
||||||
});
|
});
|
||||||
return findingEntries;
|
return findingEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FindingDialogBody {
|
||||||
|
title: string;
|
||||||
|
severity: Severity;
|
||||||
|
description: string;
|
||||||
|
impact: string;
|
||||||
|
affectedUrls: Array<string>;
|
||||||
|
reproduction: string;
|
||||||
|
mitigation: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface ProjectDialogData {
|
export interface GenericDialogData {
|
||||||
form: {
|
form: {
|
||||||
[key: string]: GenericFormFieldConfig // key is property name, e.g. title
|
[key: string]: GenericFormFieldConfig // key is property name, e.g. title
|
||||||
};
|
};
|
|
@ -0,0 +1,180 @@
|
||||||
|
<nb-card #dialog accent="{{dialogData?.options[0].accentColor}}" class="finding-dialog">
|
||||||
|
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
|
||||||
|
{{ dialogData?.options[0].headerLabelKey | translate }}
|
||||||
|
</nb-card-header>
|
||||||
|
<nb-card-body>
|
||||||
|
<form *ngIf="formArray" [formGroup]="findingFormGroup">
|
||||||
|
<div>
|
||||||
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
|
<!-- Form Text Layout -->
|
||||||
|
<div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
|
<!-- Title Form Field -->
|
||||||
|
<nb-form-field class="finding-form-field">
|
||||||
|
<label for="{{formArray[0].fieldName}}" class="label">
|
||||||
|
{{formArray[0].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="{{formArray[0].fieldName}}"
|
||||||
|
type="formText" required fullWidth
|
||||||
|
id="{{formArray[0].fieldName}}" nbInput
|
||||||
|
class="form-field form-text"
|
||||||
|
[status]="findingFormGroup.get(formArray[0].fieldName).dirty ? (findingFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[0].placeholder | translate}} *">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[0].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[0].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!-- Description Form Field -->
|
||||||
|
<nb-form-field class="finding-form-field">
|
||||||
|
<label for="{{formArray[2].fieldName}}" class="label">
|
||||||
|
{{formArray[2].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<textarea formControlName="{{formArray[2].fieldName}}"
|
||||||
|
type="formText" required fullWidth
|
||||||
|
id="{{formArray[2].fieldName}}" nbInput
|
||||||
|
class="form-field form-text"
|
||||||
|
[status]="findingFormGroup.get(formArray[2].fieldName).dirty ? (findingFormGroup.get(formArray[2].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[2].placeholder | translate}} *">
|
||||||
|
</textarea>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[2].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[2].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[2].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!-- Impact Form Field -->
|
||||||
|
<nb-form-field class="finding-form-field">
|
||||||
|
<label for="{{formArray[3].fieldName}}" class="label">
|
||||||
|
{{formArray[3].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<textarea formControlName="{{formArray[3].fieldName}}"
|
||||||
|
type="formText" required fullWidth
|
||||||
|
id="{{formArray[3].fieldName}}" nbInput
|
||||||
|
class="form-field form-text"
|
||||||
|
[status]="findingFormGroup.get(formArray[3].fieldName).dirty ? (findingFormGroup.get(formArray[3].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[3].placeholder | translate}} *">
|
||||||
|
</textarea>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[3].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[3].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[3].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
</div>
|
||||||
|
<!-- Severity Layout -->
|
||||||
|
<!-- Severity Form Field -->
|
||||||
|
<div fxFlex class="severity-dialog">
|
||||||
|
<label for="{{formArray[1].fieldName}}" class="label">
|
||||||
|
{{formArray[1].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<nb-select class="severities" placeholder="{{formArray[1].placeholder | translate}} *"
|
||||||
|
[(selected)]="formArray[1].controlsConfig[0].value"
|
||||||
|
status="{{getSeverityFillStatus(formArray[1].controlsConfig[0].value)}}" filled>
|
||||||
|
<nb-option *ngFor="let severity of severityTexts" [value]="severity.value">
|
||||||
|
{{ severity.translationText | translate }}
|
||||||
|
</nb-option>
|
||||||
|
</nb-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Affected URLs Layout -->
|
||||||
|
<!-- Affected URLs Form Field -->
|
||||||
|
<nb-form-field class="finding-form-field">
|
||||||
|
<label for="{{formArray[4].fieldName}}" class="label">
|
||||||
|
{{formArray[4].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="{{formArray[4].fieldName}}"
|
||||||
|
type="text"
|
||||||
|
id="{{formArray[4].fieldName}}"
|
||||||
|
nbTagInput fullWidth
|
||||||
|
shape="rectangle"
|
||||||
|
(tagAdd)="onAffectedUrlAdd()"
|
||||||
|
class="form-field additionalUrl"
|
||||||
|
[status]="findingFormGroup.get(formArray[4].fieldName).dirty ? (findingFormGroup.get(formArray[4].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[4].placeholder | translate}}">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[4].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[4].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[4].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!-- Add Affected URLs Button -->
|
||||||
|
<button nbButton status="primary" size="small" class="add-url-button"
|
||||||
|
(click)="onAffectedUrlAdd()">
|
||||||
|
<fa-icon [icon]="fa.faPlus" class="new-url-icon"></fa-icon>
|
||||||
|
<span> {{ 'finding.add.url' | translate }} </span>
|
||||||
|
</button>
|
||||||
|
<!---->
|
||||||
|
<nb-tag-list (tagRemove)="onAffectedUrlTagRemove($event)" class="url-tag-list">
|
||||||
|
<nb-tag status="info" appearance="outline" class="url-tag" removable *ngFor="let url of affectedUrls" [text]="url"></nb-tag>
|
||||||
|
</nb-tag-list>
|
||||||
|
<!-- Additional Text Layout -->
|
||||||
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
|
<!-- Reproduction Form Field -->
|
||||||
|
<nb-form-field fxFlex="50" class="finding-form-field">
|
||||||
|
<label for="{{formArray[5].fieldName}}" class="label">
|
||||||
|
{{formArray[5].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<textarea formControlName="{{formArray[5].fieldName}}"
|
||||||
|
type="text" required fullWidth
|
||||||
|
id="{{formArray[5].fieldName}}" nbInput
|
||||||
|
class="form-field form-textarea"
|
||||||
|
[status]="findingFormGroup.get(formArray[5].fieldName).dirty ? (findingFormGroup.get(formArray[5].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[5].placeholder | translate}} *">
|
||||||
|
</textarea>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[5].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[5].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[5].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!-- Mitigation Form Field -->
|
||||||
|
<nb-form-field fxFlex class="finding-form-field">
|
||||||
|
<label for="{{formArray[6].fieldName}}" class="label">
|
||||||
|
{{formArray[6].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<textarea formControlName="{{formArray[6].fieldName}}"
|
||||||
|
type="text" fullWidth
|
||||||
|
id="{{formArray[6].fieldName}}" nbInput
|
||||||
|
class="form-field form-textarea"
|
||||||
|
[status]="findingFormGroup.get(formArray[6].fieldName).dirty ? (findingFormGroup.get(formArray[6].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[6].placeholder | translate}}">
|
||||||
|
</textarea>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[6].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[6].fieldName).dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[6].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
|
{{error.translationKey | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</nb-card-body>
|
||||||
|
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||||
|
<button nbButton status="success" size="small" class="dialog-button" [disabled]="!allowSave()"
|
||||||
|
(click)="onClickSave(findingFormGroup.value)">
|
||||||
|
{{ dialogData?.options[0].buttonKey | translate}}
|
||||||
|
</button>
|
||||||
|
<button nbButton status="danger" size="small" class="dialog-button" (click)="onClickClose()">
|
||||||
|
{{ 'global.action.cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
</nb-card-footer>
|
||||||
|
</nb-card>
|
|
@ -0,0 +1,92 @@
|
||||||
|
@import "../../../assets/@theme/styles/_dialog.scss";
|
||||||
|
@import '../../../assets/@theme/styles/themes';
|
||||||
|
|
||||||
|
.finding-dialog {
|
||||||
|
width: 65.25rem !important;
|
||||||
|
height: 55rem;
|
||||||
|
|
||||||
|
.finding-dialog-header {
|
||||||
|
height: 8vh;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nb-form-field {
|
||||||
|
padding: 0.5rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
width: 30rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 30rem !important;
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.additionalUrl {
|
||||||
|
font-family: Courier, serif;
|
||||||
|
background-color: nb-theme(card-header-basic-background-color);
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-url-button {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
.new-url-icon {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-tag-list {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
.url-tag {
|
||||||
|
max-width: 62rem !important;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
// Style
|
||||||
|
font-family: Courier, serif;
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
float: left;
|
||||||
|
color: nb-theme(color-danger-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-dialog {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 0.5rem 0 0.75rem;
|
||||||
|
|
||||||
|
.severities {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-0 {
|
||||||
|
}
|
||||||
|
.severity-1 {
|
||||||
|
background-color: nb-theme(color-info-default);
|
||||||
|
}
|
||||||
|
.severity-2 {
|
||||||
|
background-color: nb-theme(color-warning-default);
|
||||||
|
}
|
||||||
|
.severity-3 {
|
||||||
|
background-color: nb-theme(color-danger-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {FindingDialogComponent} from './finding-dialog.component';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {
|
||||||
|
NB_DIALOG_CONFIG,
|
||||||
|
NbButtonModule,
|
||||||
|
NbCardModule,
|
||||||
|
NbDialogRef,
|
||||||
|
NbFormFieldModule,
|
||||||
|
NbInputModule,
|
||||||
|
NbLayoutModule, NbTagModule
|
||||||
|
} from '@nebular/theme';
|
||||||
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
|
import {ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {HttpLoaderFactory} from '../../../app/common-app.module';
|
||||||
|
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {NotificationService} from '@shared/services/notification.service';
|
||||||
|
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||||
|
import {Severity} from '@shared/models/severity.enum';
|
||||||
|
import {Finding} from '@shared/models/finding.model';
|
||||||
|
import Mock = jest.Mock;
|
||||||
|
import {Category} from '@shared/models/category.model';
|
||||||
|
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||||
|
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
|
||||||
|
import {NgxsModule, Store} from '@ngxs/store';
|
||||||
|
|
||||||
|
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('FindingDialogComponent', () => {
|
||||||
|
let component: FindingDialogComponent;
|
||||||
|
let fixture: ComponentFixture<FindingDialogComponent>;
|
||||||
|
let store: Store;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
FindingDialogComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NbLayoutModule,
|
||||||
|
NbCardModule,
|
||||||
|
NbButtonModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
NbInputModule,
|
||||||
|
NbFormFieldModule,
|
||||||
|
NbTagModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
ThemeModule.forRoot(),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NgxsModule.forRoot([ProjectState]),
|
||||||
|
HttpClientModule,
|
||||||
|
HttpClientTestingModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
|
{provide: NbDialogRef, useValue: dialogSpy},
|
||||||
|
{provide: NB_DIALOG_CONFIG, useValue: mockedFindingDialogData}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedFindingDialogData});
|
||||||
|
fixture = TestBed.createComponent(FindingDialogComponent);
|
||||||
|
store = TestBed.inject(Store);
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
|
||||||
|
});
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSpyObj = (baseName, methodNames): { [key: string]: Mock<any> } => {
|
||||||
|
const obj: any = {};
|
||||||
|
for (const i of methodNames) {
|
||||||
|
obj[i] = jest.fn();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockFinding: Finding = {
|
||||||
|
id: '11-22-33',
|
||||||
|
severity: Severity.LOW,
|
||||||
|
title: 'Test Finding',
|
||||||
|
description: 'Test Description',
|
||||||
|
impact: 'Test Impact',
|
||||||
|
affectedUrls: ['https://test.de'],
|
||||||
|
reproduction: 'Step N: Test',
|
||||||
|
mitigation: 'Mitigation Test'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockedFindingDialogData = {
|
||||||
|
form: {
|
||||||
|
findingTitle: {
|
||||||
|
fieldName: 'findingTitle',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.title.label',
|
||||||
|
placeholder: 'finding.title',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.title : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.titleRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingSeverity: {
|
||||||
|
fieldName: 'findingSeverity',
|
||||||
|
type: 'severity-select',
|
||||||
|
labelKey: 'finding.severity.label',
|
||||||
|
placeholder: 'finding.severity',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.severity : Severity.LOW, disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.severityRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingDescription: {
|
||||||
|
fieldName: 'findingDescription',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.description.label',
|
||||||
|
placeholder: 'finding.description',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.description : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.descriptionRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingImpact: {
|
||||||
|
fieldName: 'findingImpact',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.impact.label',
|
||||||
|
placeholder: 'finding.impact',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.impact : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.impactRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingAffectedUrls: {
|
||||||
|
fieldName: 'findingAffectedUrls',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.affectedUrls.label',
|
||||||
|
placeholder: 'finding.affectedUrls.placeholder',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: '', disabled: false},
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.affectedUrlsRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingReproduction: {
|
||||||
|
fieldName: 'findingReproduction',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.reproduction.label',
|
||||||
|
placeholder: 'finding.reproduction',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.reproduction : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.reproductionRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingMitigation: {
|
||||||
|
fieldName: 'findingMitigation',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.mitigation.label',
|
||||||
|
placeholder: 'finding.mitigation',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: mockFinding ? mockFinding.mitigation : '', disabled: false},
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.mitigationRequired'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
headerLabelKey: 'finding.create.header',
|
||||||
|
buttonKey: 'global.action.save',
|
||||||
|
accentColor: 'info'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,156 @@
|
||||||
|
import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core';
|
||||||
|
import {FormBuilder, FormGroup} from '@angular/forms';
|
||||||
|
import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic-dialog-data';
|
||||||
|
import {NB_DIALOG_CONFIG, NbDialogRef, NbTagComponent} from '@nebular/theme';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
|
import {UntilDestroy} from '@ngneat/until-destroy';
|
||||||
|
import {Severity} from '@shared/models/severity.enum';
|
||||||
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
@UntilDestroy()
|
||||||
|
@Component({
|
||||||
|
selector: 'app-finding-dialog',
|
||||||
|
templateUrl: './finding-dialog.component.html',
|
||||||
|
styleUrls: ['./finding-dialog.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class FindingDialogComponent implements OnInit {
|
||||||
|
// form control elements
|
||||||
|
findingFormGroup: FormGroup;
|
||||||
|
formArray: GenericFormFieldConfig[];
|
||||||
|
|
||||||
|
dialogData: GenericDialogData;
|
||||||
|
|
||||||
|
// HTML only
|
||||||
|
readonly fa = FA;
|
||||||
|
severity: Severity = Severity.LOW;
|
||||||
|
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'}
|
||||||
|
];
|
||||||
|
|
||||||
|
// ToDo: Adjust for edit finding dialog to include existing urls
|
||||||
|
affectedUrls: string[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
protected dialogRef: NbDialogRef<FindingDialogComponent>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.findingFormGroup = this.generateFormCreationFieldArray();
|
||||||
|
this.dialogData = this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFormCreationFieldArray(): FormGroup {
|
||||||
|
this.formArray = Object.values(this.data.form);
|
||||||
|
const config = this.formArray?.reduce((accumulator: {}, currentValue: GenericFormFieldConfig) => ({
|
||||||
|
...accumulator,
|
||||||
|
[currentValue?.fieldName]: currentValue?.controlsConfig
|
||||||
|
}), {});
|
||||||
|
return this.fb.group(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickSave(value: any): void {
|
||||||
|
this.dialogRef.close({
|
||||||
|
title: value.findingTitle,
|
||||||
|
severity: value.findingSeverity,
|
||||||
|
description: value.findingDescription,
|
||||||
|
impact: value.findingImpact,
|
||||||
|
affectedUrls: this.affectedUrls ? this.affectedUrls : [],
|
||||||
|
reproduction: value.findingReproduction,
|
||||||
|
mitigation: value.findingMitigation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAffectedUrlAdd(): void {
|
||||||
|
// tslint:disable-next-line:no-string-literal
|
||||||
|
const newUrl = this.findingFormGroup.controls['findingAffectedUrls'].value;
|
||||||
|
if (newUrl) {
|
||||||
|
this.affectedUrls.push(newUrl);
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line:no-string-literal
|
||||||
|
this.findingFormGroup.controls['findingAffectedUrls'].reset('');
|
||||||
|
}
|
||||||
|
|
||||||
|
onAffectedUrlTagRemove(tagToRemove: NbTagComponent): void {
|
||||||
|
this.affectedUrls = this.affectedUrls.filter(t => t !== tagToRemove.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickClose(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
allowSave(): boolean {
|
||||||
|
return this.findingFormGroup.valid && this.findingDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the correct nb-status for current severity
|
||||||
|
*/
|
||||||
|
getSeverityFillStatus(value: number): string {
|
||||||
|
let severityFillStatus;
|
||||||
|
switch (value) {
|
||||||
|
case 0: {
|
||||||
|
severityFillStatus = 'basic';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
severityFillStatus = 'info';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
severityFillStatus = 'warning';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
severityFillStatus = 'danger';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
severityFillStatus = 'control';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return severityFillStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if finding data is different from initial value
|
||||||
|
*/
|
||||||
|
private findingDataChanged(): boolean {
|
||||||
|
const oldFindingData = this.parseInitializedFindingDialogData(this.dialogData);
|
||||||
|
const newFindingData = this.findingFormGroup.getRawValue();
|
||||||
|
Object.entries(newFindingData).forEach(entry => {
|
||||||
|
const [key, value] = entry;
|
||||||
|
if (value === null) {
|
||||||
|
newFindingData[key] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const didChange = !deepEqual(oldFindingData, newFindingData);
|
||||||
|
return didChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dialogData of type GenericDialogData
|
||||||
|
* @return parsed findingData
|
||||||
|
*/
|
||||||
|
private parseInitializedFindingDialogData(dialogData: GenericDialogData): any {
|
||||||
|
const findingData = {};
|
||||||
|
Object.entries(dialogData.form).forEach(entry => {
|
||||||
|
const [key, value] = entry;
|
||||||
|
findingData[key] = value.controlsConfig[0] ?
|
||||||
|
(value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : '';
|
||||||
|
});
|
||||||
|
return findingData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeverityText {
|
||||||
|
value: Severity;
|
||||||
|
translationText: string;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
import {NbButtonModule, NbCardModule, NbDialogService, NbFormFieldModule, NbInputModule, NbSelectModule, NbTagModule} from '@nebular/theme';
|
||||||
|
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||||
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
|
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
FindingDialogComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NbCardModule,
|
||||||
|
NbButtonModule,
|
||||||
|
NbFormFieldModule,
|
||||||
|
NbInputModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
FontAwesomeModule,
|
||||||
|
TranslateModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NbSelectModule,
|
||||||
|
NbTagModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
DialogService,
|
||||||
|
FindingDialogService,
|
||||||
|
NbDialogService
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
FindingDialogComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class FindingDialogModule { }
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {ComponentType} from '@angular/cdk/overlay';
|
||||||
|
import {NbDialogConfig} from '@nebular/theme';
|
||||||
|
import {Observable, of} from 'rxjs';
|
||||||
|
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||||
|
import {Finding} from '@shared/models/finding.model';
|
||||||
|
|
||||||
|
export class FindingDialogServiceMock implements Required<FindingDialogService> {
|
||||||
|
|
||||||
|
dialog: any;
|
||||||
|
|
||||||
|
openFindingDialog(
|
||||||
|
componentOrTemplateRef: ComponentType<any>,
|
||||||
|
finding: Finding | undefined,
|
||||||
|
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FindingDialogService } from './finding-dialog.service';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {NbDialogModule, NbDialogRef} from '@nebular/theme';
|
||||||
|
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
|
||||||
|
|
||||||
|
describe('FindingDialogService', () => {
|
||||||
|
let service: FindingDialogService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
NbDialogModule.forRoot()
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
|
||||||
|
{provide: NbDialogRef, useValue: {}},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(FindingDialogService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,155 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {NbDialogConfig, NbDialogService} from '@nebular/theme';
|
||||||
|
import {GenericDialogData} from '@shared/models/generic-dialog-data';
|
||||||
|
import {ComponentType} from '@angular/cdk/overlay';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {Validators} from '@angular/forms';
|
||||||
|
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
||||||
|
import {Finding} from '@shared/models/finding.model';
|
||||||
|
import {Severity} from '@shared/models/severity.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FindingDialogService {
|
||||||
|
|
||||||
|
constructor(private readonly dialog: NbDialogService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly MIN_LENGTH: number = 4;
|
||||||
|
|
||||||
|
static addDataToDialogConfig(
|
||||||
|
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
|
||||||
|
findingData?: GenericDialogData
|
||||||
|
): Partial<NbDialogConfig<Partial<any> | string>> {
|
||||||
|
return {
|
||||||
|
context: {data: findingData},
|
||||||
|
closeOnEsc: dialogOptions?.closeOnEsc || false,
|
||||||
|
hasScroll: dialogOptions?.hasScroll || false,
|
||||||
|
autoFocus: dialogOptions?.autoFocus || false,
|
||||||
|
closeOnBackdropClick: dialogOptions?.closeOnBackdropClick || false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public openFindingDialog(componentOrTemplateRef: ComponentType<any>,
|
||||||
|
finding?: Finding,
|
||||||
|
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
||||||
|
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
||||||
|
let dialogData: GenericDialogData;
|
||||||
|
// Setup FindingDialogBody
|
||||||
|
dialogData = {
|
||||||
|
form: {
|
||||||
|
findingTitle: {
|
||||||
|
fieldName: 'findingTitle',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.title.label',
|
||||||
|
placeholder: 'finding.title',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.title : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.titleRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingSeverity: {
|
||||||
|
fieldName: 'findingSeverity',
|
||||||
|
type: 'severity-select',
|
||||||
|
labelKey: 'finding.severity.label',
|
||||||
|
placeholder: 'finding.severity',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.severity : Severity.LOW, disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.severityRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingDescription: {
|
||||||
|
fieldName: 'findingDescription',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.description.label',
|
||||||
|
placeholder: 'finding.description',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.description : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.descriptionRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingImpact: {
|
||||||
|
fieldName: 'findingImpact',
|
||||||
|
type: 'formText',
|
||||||
|
labelKey: 'finding.impact.label',
|
||||||
|
placeholder: 'finding.impact',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.impact : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.impactRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingAffectedUrls: {
|
||||||
|
fieldName: 'findingAffectedUrls',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.affectedUrls.label',
|
||||||
|
placeholder: 'finding.affectedUrls.placeholder',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: '', disabled: false},
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.affectedUrlsRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingReproduction: {
|
||||||
|
fieldName: 'findingReproduction',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.reproduction.label',
|
||||||
|
placeholder: 'finding.reproduction',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.reproduction : '', disabled: false},
|
||||||
|
[Validators.required]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.reproductionRequired'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
findingMitigation: {
|
||||||
|
fieldName: 'findingMitigation',
|
||||||
|
type: 'text',
|
||||||
|
labelKey: 'finding.mitigation.label',
|
||||||
|
placeholder: 'finding.mitigation',
|
||||||
|
controlsConfig: [
|
||||||
|
{value: finding ? finding.mitigation : '', disabled: false},
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
errors: [
|
||||||
|
{errorCode: 'required', translationKey: 'finding.validationMessage.mitigationRequired'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: []
|
||||||
|
};
|
||||||
|
if (finding) {
|
||||||
|
dialogData.options = [
|
||||||
|
{
|
||||||
|
headerLabelKey: 'finding.edit.header',
|
||||||
|
buttonKey: 'global.action.update',
|
||||||
|
accentColor: 'warning'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
dialogData.options = [
|
||||||
|
{
|
||||||
|
headerLabelKey: 'finding.create.header',
|
||||||
|
buttonKey: 'global.action.save',
|
||||||
|
accentColor: 'info'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Merge dialog config with finding data
|
||||||
|
dialogOptions = FindingDialogService.addDataToDialogConfig(config, dialogData);
|
||||||
|
return this.dialog.open(FindingDialogComponent, dialogOptions).onClose;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
<input formControlName="{{fieldConfig.fieldName}}"
|
<input formControlName="{{fieldConfig.fieldName}}"
|
||||||
type="text" required fullWidth
|
type="text" required fullWidth
|
||||||
id="{{fieldConfig.fieldName}}" nbInput
|
id="{{fieldConfig.fieldName}}" nbInput
|
||||||
class="input"
|
class="form-field"
|
||||||
[status]="projectFormGroup.get(fieldConfig.fieldName).dirty ? (projectFormGroup.get(fieldConfig.fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
[status]="projectFormGroup.get(fieldConfig.fieldName).dirty ? (projectFormGroup.get(fieldConfig.fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
placeholder="{{fieldConfig.placeholder | translate}} *">
|
placeholder="{{fieldConfig.placeholder | translate}} *">
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.form-field {
|
||||||
width: 18rem;
|
width: 18rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.
|
||||||
import {ReactiveFormsModule, Validators} from '@angular/forms';
|
import {ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
import {Project} from '@shared/models/project.model';
|
import {Project} from '@shared/models/project.model';
|
||||||
import Mock = jest.Mock;
|
import Mock = jest.Mock;
|
||||||
import deepEqual from 'deep-equal';
|
|
||||||
|
|
||||||
describe('ProjectDialogComponent', () => {
|
describe('ProjectDialogComponent', () => {
|
||||||
let component: ProjectDialogComponent;
|
let component: ProjectDialogComponent;
|
||||||
|
@ -62,13 +61,13 @@ describe('ProjectDialogComponent', () => {
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
{provide: DialogService, useClass: DialogServiceMock},
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
{provide: NbDialogRef, useValue: dialogSpy},
|
{provide: NbDialogRef, useValue: dialogSpy},
|
||||||
{provide: NB_DIALOG_CONFIG, useValue: mockedDialogData}
|
{provide: NB_DIALOG_CONFIG, useValue: mockedProjectDialogData}
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedDialogData});
|
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedProjectDialogData});
|
||||||
fixture = TestBed.createComponent(ProjectDialogComponent);
|
fixture = TestBed.createComponent(ProjectDialogComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -97,7 +96,7 @@ export const mockProject: Project = {
|
||||||
createdBy: 'UID-11-12-13'
|
createdBy: 'UID-11-12-13'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockedDialogData = {
|
export const mockedProjectDialogData = {
|
||||||
form: {
|
form: {
|
||||||
projectTitle: {
|
projectTitle: {
|
||||||
fieldName: 'projectTitle',
|
fieldName: 'projectTitle',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Component, Inject, OnInit} from '@angular/core';
|
import {Component, Inject, OnInit} from '@angular/core';
|
||||||
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
|
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
|
||||||
import {FormBuilder, FormGroup} from '@angular/forms';
|
import {FormBuilder, FormGroup} from '@angular/forms';
|
||||||
import {GenericFormFieldConfig, ProjectDialogData} from '@shared/models/project-dialog-data';
|
import {GenericFormFieldConfig, GenericDialogData} from '@shared/models/generic-dialog-data';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import {UntilDestroy} from '@ngneat/until-destroy';
|
import {UntilDestroy} from '@ngneat/until-destroy';
|
||||||
|
|
||||||
|
@ -16,10 +16,10 @@ export class ProjectDialogComponent implements OnInit {
|
||||||
projectFormGroup: FormGroup;
|
projectFormGroup: FormGroup;
|
||||||
formArray: GenericFormFieldConfig[];
|
formArray: GenericFormFieldConfig[];
|
||||||
|
|
||||||
dialogData: ProjectDialogData;
|
dialogData: GenericDialogData;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NB_DIALOG_CONFIG) private data: ProjectDialogData,
|
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
protected dialogRef: NbDialogRef<ProjectDialogComponent>
|
protected dialogRef: NbDialogRef<ProjectDialogComponent>
|
||||||
) {
|
) {
|
||||||
|
@ -75,7 +75,7 @@ export class ProjectDialogComponent implements OnInit {
|
||||||
* @param dialogData of type ProjectDialogData
|
* @param dialogData of type ProjectDialogData
|
||||||
* @return parsed projectData
|
* @return parsed projectData
|
||||||
*/
|
*/
|
||||||
private parseInitializedProjectDialogData(dialogData: ProjectDialogData): any {
|
private parseInitializedProjectDialogData(dialogData: GenericDialogData): any {
|
||||||
const projectData = {};
|
const projectData = {};
|
||||||
Object.entries(dialogData.form).forEach(entry => {
|
Object.entries(dialogData.form).forEach(entry => {
|
||||||
const [key, value] = entry;
|
const [key, value] = entry;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {Observable} from 'rxjs';
|
||||||
import {Project} from '@shared/models/project.model';
|
import {Project} from '@shared/models/project.model';
|
||||||
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
|
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
|
||||||
import {Validators} from '@angular/forms';
|
import {Validators} from '@angular/forms';
|
||||||
import {ProjectDialogData} from '@shared/models/project-dialog-data';
|
import {GenericDialogData} from '@shared/models/generic-dialog-data';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProjectDialogService {
|
export class ProjectDialogService {
|
||||||
|
@ -17,7 +17,7 @@ export class ProjectDialogService {
|
||||||
|
|
||||||
static addDataToDialogConfig(
|
static addDataToDialogConfig(
|
||||||
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
|
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
|
||||||
projectData?: ProjectDialogData
|
projectData?: GenericDialogData
|
||||||
): Partial<NbDialogConfig<Partial<any> | string>> {
|
): Partial<NbDialogConfig<Partial<any> | string>> {
|
||||||
return {
|
return {
|
||||||
context: {data: projectData},
|
context: {data: projectData},
|
||||||
|
@ -32,7 +32,7 @@ export class ProjectDialogService {
|
||||||
project?: Project,
|
project?: Project,
|
||||||
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
||||||
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
||||||
let dialogData: ProjectDialogData;
|
let dialogData: GenericDialogData;
|
||||||
// Setup ProjectDialogData
|
// Setup ProjectDialogData
|
||||||
dialogData = {
|
dialogData = {
|
||||||
form: {
|
form: {
|
||||||
|
@ -91,7 +91,7 @@ export class ProjectDialogService {
|
||||||
{
|
{
|
||||||
headerLabelKey: 'project.create.header',
|
headerLabelKey: 'project.create.header',
|
||||||
buttonKey: 'global.action.save',
|
buttonKey: 'global.action.save',
|
||||||
accentColor: 'primary'
|
accentColor: 'info'
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ 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 {Finding, FindingDialogBody} from '@shared/models/finding.model';
|
||||||
import {Severity} from '@shared/models/severity.enum';
|
import {Severity} from '@shared/models/severity.enum';
|
||||||
import {Comment} from '@shared/models/comment.model';
|
import {Comment} from '@shared/models/comment.model';
|
||||||
|
|
||||||
|
@ -68,32 +68,49 @@ export class PentestService {
|
||||||
return of([
|
return of([
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
impact: 'fucked up a lot man. better fix it',
|
description: 'test',
|
||||||
|
impact: 'This impacts only the UI',
|
||||||
severity: Severity.LOW,
|
severity: Severity.LOW,
|
||||||
|
reproduction: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
impact: 'fucked up a lot man. better fix it',
|
description: 'test',
|
||||||
|
impact: 'This is impacts some things',
|
||||||
severity: Severity.MEDIUM,
|
severity: Severity.MEDIUM,
|
||||||
|
reproduction: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
impact: 'fucked up a lot man. better fix it',
|
description: 'test',
|
||||||
|
impact: 'This is impacts a lot',
|
||||||
severity: Severity.HIGH,
|
severity: Severity.HIGH,
|
||||||
|
reproduction: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
impact: 'fucked up a lot man. better fix it',
|
description: 'test',
|
||||||
|
impact: 'This is impacts a lot',
|
||||||
severity: Severity.CRITICAL,
|
severity: Severity.CRITICAL,
|
||||||
|
reproduction: ''
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save Finding
|
||||||
|
* @param pentestId the id of the pentest
|
||||||
|
* @param finding the information of the finding
|
||||||
|
*/
|
||||||
|
public saveFinding(pentestId: string, finding: FindingDialogBody): Observable<Finding> {
|
||||||
|
return this.http.post<Finding>(`${this.apiBaseURL}/${pentestId}/finding`, finding);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Comments for Pentest Id
|
* Get Comments for Pentest Id
|
||||||
* @param pentestId the id of the project
|
* @param pentestId the id of the project
|
||||||
|
@ -108,14 +125,14 @@ export class PentestService {
|
||||||
return of([
|
return of([
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd2',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd2',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
description: 'fucked up a lot man. better fix it',
|
description: 'This is a creative description',
|
||||||
relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'],
|
relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||||
title: 'This is a lit test finding ma brother',
|
title: 'This is a creative title',
|
||||||
description: 'fucked up a lot man. better fix it',
|
description: 'This is a creative description',
|
||||||
relatedFindings: [],
|
relatedFindings: [],
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -4,9 +4,7 @@ import {StatusTagComponent} from './status-tag.component';
|
||||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
import {HttpLoaderFactory} from '../../../app/common-app.module';
|
import {HttpLoaderFactory} from '../../../app/common-app.module';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {NbCardModule, NbFocusMonitor, NbTagModule} from '@nebular/theme';
|
import {NbCardModule, NbTagModule} from '@nebular/theme';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
import {MockModule} from 'ng-mocks';
|
import {MockModule} from 'ng-mocks';
|
||||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package finding
|
package finding
|
||||||
|
|
||||||
import com.securityc4po.api.BaseEntity
|
import com.securityc4po.api.BaseEntity
|
||||||
import comment.Comment
|
|
||||||
import org.springframework.data.mongodb.core.mapping.Document
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
|
||||||
@Document(collection = "findings")
|
@Document(collection = "findings")
|
||||||
|
|
Loading…
Reference in New Issue