feat: As a user I want to add a comment via dialog

This commit is contained in:
Marcel Haag 2022-12-16 13:48:45 +01:00 committed by Cel
parent 6a625349c8
commit 46f79dcf89
38 changed files with 2136 additions and 213 deletions

View File

@ -18,6 +18,10 @@ import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {MockComponent} from 'ng-mocks'; import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {
@ -74,7 +78,9 @@ describe('PentestCommentsComponent', () => {
NgxsModule.forRoot([ProjectState]) NgxsModule.forRoot([ProjectState])
], ],
providers: [ providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()} {provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -3,15 +3,25 @@ import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {PentestService} from '@shared/services/pentest.service'; import {NotificationService, PopupType} from '@shared/services/notification.service';
import {NotificationService} from '@shared/services/notification.service';
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 {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model'; import {
Comment,
CommentDialogBody,
CommentEntry,
transformCommentsToObjectiveEntries,
transformCommentToRequestBody
} from '@shared/models/comment.model';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentService} from '@shared/services/comment.service';
import {UpdatePentestComments, UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -42,10 +52,12 @@ export class PentestCommentsComponent implements OnInit {
expandedGetter: (node: CommentEntry) => !!node.expanded, expandedGetter: (node: CommentEntry) => !!node.expanded,
}; };
constructor(private readonly pentestService: PentestService, constructor(private readonly commentService: CommentService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>, private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
private store: Store, private notificationService: NotificationService,
private notificationService: NotificationService) { private dialogService: DialogService,
private commentDialogService: CommentDialogService,
private store: Store) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters); this.dataSource = dataSourceBuilder.create(this.data, this.getters);
} }
@ -64,7 +76,7 @@ export class PentestCommentsComponent implements OnInit {
} }
loadCommentsData(): void { loadCommentsData(): void {
this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe( .pipe(
untilDestroyed(this), untilDestroyed(this),
/*filter(isNotNullOrUndefined),*/ /*filter(isNotNullOrUndefined),*/
@ -90,15 +102,47 @@ export class PentestCommentsComponent implements OnInit {
} }
onClickAddComment(): void { onClickAddComment(): void {
console.info('Coming soon..'); this.commentDialogService.openCommentDialog(
FindingDialogComponent,
this.pentestInfo$.getValue().findingIds,
null,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
tap((value) => console.warn('CommentDialogBody: ', value)),
mergeMap((value: CommentDialogBody) =>
this.commentService.saveComment(
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
transformCommentToRequestBody(value)
)
),
untilDestroyed(this)
).subscribe({
next: (newComment: Comment) => {
this.store.dispatch(new UpdatePentestComments(newComment.id));
this.loadCommentsData();
// Todo: Fix trans keys
this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS);
},
error: err => {
console.error(err);
// Todo: Fix trans keys
this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE);
}
});
} }
onClickEditComment(comment): void { onClickEditComment(commentEntry): void {
console.info('Coming soon..'); console.info('Coming soon..', commentEntry);
} }
onClickDeleteComment(comment): void { onClickDeleteComment(commentEntry): void {
console.info('Coming soon..'); console.info('Coming soon..', commentEntry);
} }
// HTML only // HTML only

View File

@ -3,18 +3,19 @@ import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {PentestComponent} from './pentest.component'; import {PentestComponent} from './pentest.component';
import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme'; import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
import { PentestHeaderComponent } from './pentest-header/pentest-header.component'; import {PentestHeaderComponent} from './pentest-header/pentest-header.component';
import { PentestContentComponent } from './pentest-content/pentest-content.component'; import {PentestContentComponent} from './pentest-content/pentest-content.component';
import {FlexLayoutModule} from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module'; 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 {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'; import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
import {CommentDialogModule} from '@shared/modules/comment-dialog/comment-dialog.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -25,26 +26,28 @@ import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog
PentestFindingsComponent, PentestFindingsComponent,
PentestCommentsComponent PentestCommentsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
CommonAppModule, CommonAppModule,
RouterModule.forChild([{ RouterModule.forChild([{
path: '', path: '',
component: PentestComponent component: PentestComponent
}]), }]),
NbLayoutModule, NbLayoutModule,
NbCardModule, NbCardModule,
FlexLayoutModule, FlexLayoutModule,
FontAwesomeModule, FontAwesomeModule,
TranslateModule, TranslateModule,
NbButtonModule, NbButtonModule,
StatusTagModule, StatusTagModule,
NbTabsetModule, NbTabsetModule,
NbTreeGridModule, NbTreeGridModule,
SeverityTagModule, SeverityTagModule,
FindingDialogModule, NbSelectModule,
NbSelectModule // Dialog Modules
] FindingDialogModule,
CommentDialogModule,
]
}) })
export class PentestModule { export class PentestModule {
} }

View File

@ -10,6 +10,7 @@
"action.exit": "Beenden", "action.exit": "Beenden",
"action.update": "Speichern", "action.update": "Speichern",
"action.export": "Exportieren", "action.export": "Exportieren",
"action.reset": "Zurücksetzen",
"action.yes": "Ja", "action.yes": "Ja",
"action.no": "Nein", "action.no": "Nein",
"username": "Nutzername", "username": "Nutzername",
@ -148,10 +149,31 @@
"commentId": "Kommentar Id", "commentId": "Kommentar Id",
"title": "Titel", "title": "Titel",
"description": "Beschreibung", "description": "Beschreibung",
"relatedFindings": "Verwandte Funde", "relatedFindings": "Verbundene Funde",
"add": "Kommentar hinzufügen", "add": "Kommentar hinzufügen",
"no.relatedFindings": "Nicht verbunden mit Fund", "add.finding": "Fund hinzufügen",
"no.comments": "Keine Kommentare verfügbar", "no.comments": "Keine Kommentare verfügbar",
"no.relatedFindings": "Nicht verbunden mit Fund",
"relatedFindingsPlaceholder": "Fund auswählen",
"noFindingsInObjectivePlaceholder": "Objective hat keine Befunde, auf die es sich beziehen könnte.",
"create": {
"header": "Neuen Kommentar erstellen"
},
"edit": {
"header": "Kommentar editieren"
},
"delete": {
"title": "Kommentar löschen",
"key": "Möchten Sie den Kommentar \"{{name}}\" unwiderruflich löschen?"
},
"title.label": "Kommentartitel",
"description.label": "Beschreibung des Kommentars",
"relatedFindings.label": "Verbundene Funde",
"validationMessage": {
"titleRequired": "Titel ist erforderlich.",
"descriptionRequired": "Beschreibung ist erforderlich.",
"relatedFindings": "Verwandte Funde erforderlich."
},
"popup": { "popup": {
"not.found": "Keine Kommentare gefunden", "not.found": "Keine Kommentare gefunden",
"save.success": "Kommentar erfolgreich gespeichert", "save.success": "Kommentar erfolgreich gespeichert",

View File

@ -10,6 +10,7 @@
"action.exit": "Exit", "action.exit": "Exit",
"action.update": "Update", "action.update": "Update",
"action.export": "Export", "action.export": "Export",
"action.reset": "Reset",
"action.yes": "Yes", "action.yes": "Yes",
"action.no": "No", "action.no": "No",
"username": "Username", "username": "Username",
@ -150,8 +151,29 @@
"description": "Description", "description": "Description",
"relatedFindings": "Related Findings", "relatedFindings": "Related Findings",
"add": "Add Comment", "add": "Add Comment",
"add.finding": "Add related finding",
"no.comments": "No comments available", "no.comments": "No comments available",
"no.relatedFindings": "Not related to finding", "no.relatedFindings": "Not related to finding",
"relatedFindingsPlaceholder": "Select a related finding",
"noFindingsInObjectivePlaceholder": "Objective doesn't have any findings to relate to.",
"create": {
"header": "Create New Comment"
},
"edit": {
"header": "Edit Comment"
},
"delete": {
"title": "Delete Comment",
"key": "Do you want to permanently delete the comment \"{{name}}\"?"
},
"title.label": "Comment Title",
"description.label": "Description of Comment",
"relatedFindings.label": "Related Findings",
"validationMessage": {
"titleRequired": "Title is required.",
"descriptionRequired": "Description is required.",
"relatedFindings": "Related findings required."
},
"popup": { "popup": {
"not.found": "No comment found", "not.found": "No comment found",
"save.success": "Comment saved successfully", "save.success": "Comment saved successfully",

View File

@ -42,3 +42,30 @@ export function transformCommentsToObjectiveEntries(findings: Comment[]): Commen
}); });
return findingEntries; return findingEntries;
} }
export function transformCommentToRequestBody(comment: CommentDialogBody | Comment): Comment {
const transformedComment = {
...comment,
title: comment.title,
description: comment.description,
// Transforms related findings from RelatedFindingOption to list of finding ids
relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.value.id) : [],
/* Remove Table Entry Object Properties */
childEntries: undefined,
kind: undefined,
findings: undefined,
expanded: undefined,
} as unknown as Comment;
return transformedComment;
}
export interface CommentDialogBody {
title: string;
description: string;
relatedFindings: Array<RelatedFindingOption>;
}
export interface RelatedFindingOption {
id: string;
title: string;
}

View File

@ -0,0 +1,80 @@
<nb-card #dialog accent="{{dialogData?.options[0].accentColor}}" class="comment-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]="commentFormGroup">
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
<!-- Form Text Layout -->
<div fxLayout="column" fxFlex fxLayoutGap="1rem" fxLayoutAlign="start start">
<!-- Title Form Field -->
<nb-form-field class="comment-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]="commentFormGroup.get(formArray[0].fieldName).dirty ? (commentFormGroup.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="commentFormGroup.get(formArray[0].fieldName).dirty">
<span class="error-text"
*ngIf="commentFormGroup.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="comment-form-field">
<label for="{{formArray[1].fieldName}}" class="label">
{{formArray[1].labelKey | translate}}
</label>
<textarea formControlName="{{formArray[1].fieldName}}"
type="formText" required fullWidth
id="{{formArray[1].fieldName}}" nbInput
class="form-field form-textarea"
[status]="commentFormGroup.get(formArray[1].fieldName).dirty ? (commentFormGroup.get(formArray[1].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{formArray[1].placeholder | translate}} *">
</textarea>
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
<ng-template ngFor let-error [ngForOf]="formArray[1].errors"
*ngIf="commentFormGroup.get(formArray[1].fieldName).dirty">
<span class="error-text"
*ngIf="commentFormGroup.get(formArray[1].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}}
</span>
</ng-template>
</nb-form-field>
</div>
</div>
<!-- Related Findings Layout -->
<!-- Related Findings Form Field -->
<nb-form-field class="comment-form-field">
<label for="{{formArray[2].fieldName}}" class="label">
{{formArray[2].labelKey | translate}}
</label>
<!--<fa-icon nbPrefix [icon]="fa.faExclamationCircle" size="lg" class="finding-icon"></fa-icon>-->
<nb-select placeholder="{{formArray[2].placeholder | translate}}" formControlName="{{formArray[2].fieldName}}" multiple
(selectedChange)="changeSelected($event)"
fullWidth shape="semi-round" filled status="info" [size]="'large'" class="form-field relatedFindings">
<nb-option class="reset-option">{{'global.action.reset' | translate}}</nb-option>
<nb-option class="finding-option" *ngFor="let finding of relatedFindings" [value]="{value: finding}">
{{finding.title}}
</nb-option>
</nb-select>
</nb-form-field>
</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(commentFormGroup.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>

View File

@ -0,0 +1,65 @@
@import "../../../assets/@theme/styles/_dialog.scss";
@import '../../../assets/@theme/styles/themes';
.comment-dialog {
width: 45.25rem !important;
height: 45rem;
.comment-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: 42rem !important;
}
.form-textarea {
width: 42rem !important;
height: 8rem;
}
.comment-form-field {
align-items: center;
.finding-icon {
color: nb-theme(color-danger-default);
}
}
.relatedFindings {
// font-family: Courier, serif;
// background-color: nb-theme(card-header-basic-background-color);
width: 100% !important;
.finding-option {
color: nb-theme(color-danger-default) !important;
background-color: nb-theme(color-danger-default) !important;
}
.reset-option {
color: nb-theme(color-danger-default) !important;
background-color: nb-theme(color-danger-default) !important;
}
}
.error-text {
float: left;
color: nb-theme(color-danger-default);
}
}

View File

@ -0,0 +1,188 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CommentDialogComponent} from './comment-dialog.component';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NgxsModule, Store} from '@ngxs/store';
import {CommonModule} from '@angular/common';
import {
NB_DIALOG_CONFIG,
NbButtonModule,
NbCardModule,
NbDialogRef,
NbFormFieldModule,
NbInputModule,
NbLayoutModule, NbSelectModule,
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 Mock = jest.Mock;
import {Finding} from '@shared/models/finding.model';
import {Severity} from '@shared/models/severity.enum';
import {Comment} from '@shared/models/comment.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,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('CommentDialogComponent', () => {
let component: CommentDialogComponent;
let fixture: ComponentFixture<CommentDialogComponent>;
let store: Store;
beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({
declarations: [
CommentDialogComponent
],
imports: [
CommonModule,
NbLayoutModule,
NbCardModule,
NbButtonModule,
FlexLayoutModule,
NbInputModule,
NbFormFieldModule,
NbTagModule,
NbSelectModule,
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: mockedCommentDialogData}
]
}).compileComponents();
});
beforeEach(() => {
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData});
fixture = TestBed.createComponent(CommentDialogComponent);
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 mockComment: Comment = {
id: '11-22-33',
title: 'Test Finding',
description: 'Test Description',
relatedFindings: ['68c47c56-3bcd-45f1-a05b-c197dbd33224']
};
export const mockedCommentDialogData = {
form: {
commentTitle: {
fieldName: 'commentTitle',
type: 'formText',
labelKey: 'comment.title.label',
placeholder: 'comment.title',
controlsConfig: [
{value: mockComment ? mockComment.title : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'comment.validationMessage.titleRequired'}
]
},
commentDescription: {
fieldName: 'commentDescription',
type: 'formText',
labelKey: 'comment.description.label',
placeholder: 'comment.description',
controlsConfig: [
{value: mockComment ? mockComment.description : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'comment.validationMessage.descriptionRequired'}
]
},
commentRelatedFindings: {
fieldName: 'commentRelatedFindings',
type: 'text',
labelKey: 'comment.relatedFindings.label',
placeholder: 'comment.relatedFindingsPlaceholder',
controlsConfig: [
{value: mockComment ? mockComment.relatedFindings : [], disabled: false},
[]
],
errors: [
{errorCode: 'required', translationKey: 'finding.validationMessage.relatedFindings'}
]
}
},
options: [
{
headerLabelKey: 'comment.create.header',
buttonKey: 'global.action.save',
accentColor: 'info'
},
]
};

View File

@ -0,0 +1,170 @@
import {ChangeDetectionStrategy, Component, Inject, OnChanges, OnInit} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic-dialog-data';
import * as FA from '@fortawesome/free-solid-svg-icons';
import deepEqual from 'deep-equal';
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
import {PentestService} from '@shared/services/pentest.service';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Pentest} from '@shared/models/pentest.model';
import {Store} from '@ngxs/store';
import {Finding} from '@shared/models/finding.model';
import {RelatedFindingOption} from '@shared/models/comment.model';
import {BehaviorSubject} from 'rxjs';
@Component({
selector: 'app-comment-dialog',
templateUrl: './comment-dialog.component.html',
styleUrls: ['./comment-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
@UntilDestroy()
export class CommentDialogComponent implements OnInit {
// form control elements
commentFormGroup: FormGroup;
formArray: GenericFormFieldConfig[];
dialogData: GenericDialogData;
// HTML only
readonly fa = FA;
relatedFindings: RelatedFindingOption[] = [];
// Includes the findings that got selected as an option
selectedFindings: RelatedFindingOption[] = [];
initialSelectedFindings: RelatedFindingOption[] = [];
constructor(
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
private fb: FormBuilder,
protected dialogRef: NbDialogRef<CommentDialogComponent>,
private readonly pentestService: PentestService,
private store: Store
) {
}
ngOnInit(): void {
this.commentFormGroup = this.generateFormCreationFieldArray();
this.dialogData = this.data;
// Resets related Findings input fields when comment was found in dialog context
// tslint:disable-next-line:no-string-literal
// this.commentFormGroup.controls['commentRelatedFindings'].reset('');
}
changeSelected($event): void {
// Latest Value
console.info($event[$event.length - 1].value);
// use this element at the end
// tslint:disable-next-line:no-string-literal
console.warn(this.commentFormGroup.controls['commentRelatedFindings'].value);
// tslint:disable-next-line:no-string-literal
this.selectedFindings = this.commentFormGroup.controls['commentRelatedFindings'].value;
}
generateFormCreationFieldArray(): FormGroup {
this.formArray = Object.values(this.data.form);
const config = this.formArray?.reduce((accumulator: {}, currentValue: GenericFormFieldConfig) => ({
...accumulator,
[currentValue?.fieldName]: currentValue?.controlsConfig
}), {});
// tslint:disable-next-line:no-string-literal
const relatedFindings = this.data.form['commentRelatedFindings'].controlsConfig[0].value;
if (relatedFindings && relatedFindings.length > 0) {
// ToDo: Select included findings here (selectedFindings / initialSelectedFindings)
console.warn('IF (EDIT)', relatedFindings);
this.renderRelatedFindings(relatedFindings);
} else {
this.renderRelatedFindings([]);
}
return this.fb.group(config);
}
renderRelatedFindings(relatedFindings: any): void {
this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
// console.warn(selectedPentest.findingIds);
this.requestRelatedFindingsData(selectedPentest.id, relatedFindings);
},
error: err => {
console.error(err);
}
});
}
requestRelatedFindingsData(pentestId: string, relatedFindings: any): void {
this.pentestService.getFindingsByPentestId(pentestId).pipe(
untilDestroyed(this)
).subscribe({
next: (findings: Finding[]) => {
findings.forEach(finding => this.relatedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
// ToDo: Only add the findings that were included in
console.info('initialSelectedFindings OnINIT ', relatedFindings);
// findings.forEach(finding => this.initialSelectedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
},
error: err => {
console.error(err);
}
});
}
onClickSave(value: any): void {
this.dialogRef.close({
title: value.commentTitle,
description: value.commentDescription,
// ToDo: Refactor this to only include the ids this.commentFormGroup.controls['commentRelatedFindings'].value
relatedFindings: this.selectedFindings ? this.selectedFindings : []
});
}
onClickClose(): void {
this.dialogRef.close();
}
allowSave(): boolean {
return this.commentFormGroup.valid && this.commentDataChanged();
}
/**
* @return true if comment data is different from initial value
*/
private commentDataChanged(): boolean {
const oldCommentData = this.parseInitializedCommentDialogData(this.dialogData);
const newCommentData = this.commentFormGroup.getRawValue();
Object.entries(newCommentData).forEach(entry => {
const [key, value] = entry;
// Related Findings form field can be ignored since changes here will be recognised inside commentRelatedFindings of tag-list
if (value === null || key === 'commentRelatedFindings') {
newCommentData[key] = '';
}
});
/* ToDo: Use for EDIT implementation
console.info('initialSelectedFindings: ', this.initialSelectedFindings);
console.info('selectedFindings: ', this.selectedFindings);
console.info('deepEqual related Findings: ', deepEqual(this.initialSelectedFindings, this.selectedFindings));
*/
const didChange = !deepEqual(oldCommentData, newCommentData) || !deepEqual(this.initialSelectedFindings, this.selectedFindings);
return didChange;
}
/**
* @param dialogData of type GenericDialogData
* @return parsed findingData
*/
private parseInitializedCommentDialogData(dialogData: GenericDialogData): any {
const findingData = {};
Object.entries(dialogData.form).forEach(entry => {
const [key, value] = entry;
// console.info(key);
findingData[key] = value.controlsConfig[0] ?
(value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : '';
// Related Findings form field can be ignored since changes here will be recognised inside commentRelatedFindings of tag-list
if (key === 'commentRelatedFindings') {
findingData[key] = '';
}
});
return findingData;
}
}

View File

@ -0,0 +1,39 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommonAppModule} from '../../../app/common-app.module';
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbSelectModule, NbTagModule} from '@nebular/theme';
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: [
CommentDialogComponent
],
imports: [
CommonModule,
CommonAppModule,
NbCardModule,
NbButtonModule,
NbFormFieldModule,
NbInputModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
NbSelectModule,
NbTagModule,
ReactiveFormsModule,
],
providers: [
CommentDialogService,
],
entryComponents: [
CommentDialogComponent
]
})
export class CommentDialogModule {
}

View File

@ -0,0 +1,18 @@
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {ComponentType} from '@angular/cdk/overlay';
import {NbDialogConfig} from '@nebular/theme';
import {Observable, of} from 'rxjs';
import {Comment} from '@shared/models/comment.model';
export class CommentDialogServiceMock implements Required<CommentDialogService> {
dialog: any;
openCommentDialog(
componentOrTemplateRef: ComponentType<any>,
findingIds: [],
comment: Comment | undefined,
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
return of(undefined);
}
}

View File

@ -0,0 +1,30 @@
import { TestBed } from '@angular/core/testing';
import { CommentDialogService } from './comment-dialog.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NbDialogModule, NbDialogRef} from '@nebular/theme';
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
describe('CommentDialogService', () => {
let service: CommentDialogService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
NbDialogModule.forChild()
],
providers: [
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
{provide: NbDialogRef, useValue: {}},
]
});
service = TestBed.inject(CommentDialogService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
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 {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Comment} from '@shared/models/comment.model';
@Injectable()
export class CommentDialogService {
constructor(private readonly dialog: NbDialogService) {
}
private readonly MIN_LENGTH: number = 4;
static addDataToDialogConfig(
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
commentData?: GenericDialogData
): Partial<NbDialogConfig<Partial<any> | string>> {
return {
context: {data: commentData},
closeOnEsc: dialogOptions?.closeOnEsc || false,
hasScroll: dialogOptions?.hasScroll || false,
autoFocus: dialogOptions?.autoFocus || false,
closeOnBackdropClick: dialogOptions?.closeOnBackdropClick || false
};
}
public openCommentDialog(componentOrTemplateRef: ComponentType<any>,
findingIds: string[],
comment?: Comment,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
let dialogData: GenericDialogData;
// Setup CommentDialogBody
dialogData = {
form: {
commentTitle: {
fieldName: 'commentTitle',
type: 'formText',
labelKey: 'comment.title.label',
placeholder: 'comment.title',
controlsConfig: [
{value: comment ? comment.title : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'comment.validationMessage.titleRequired'}
]
},
commentDescription: {
fieldName: 'commentDescription',
type: 'formText',
labelKey: 'comment.description.label',
placeholder: 'comment.description',
controlsConfig: [
{value: comment ? comment.description : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'comment.validationMessage.descriptionRequired'}
]
},
commentRelatedFindings: {
fieldName: 'commentRelatedFindings',
type: 'text',
labelKey: 'comment.relatedFindings.label',
placeholder: findingIds.length === 0 ? 'comment.noFindingsInObjectivePlaceholder' : 'comment.relatedFindingsPlaceholder',
controlsConfig: [
{value: comment ? comment.relatedFindings : [], disabled: findingIds.length === 0},
[]
],
errors: [
{errorCode: 'required', translationKey: 'finding.validationMessage.relatedFindings'}
]
}
},
options: []
};
if (comment) {
dialogData.options = [
{
headerLabelKey: 'comment.edit.header',
buttonKey: 'global.action.update',
accentColor: 'warning'
},
];
} else {
dialogData.options = [
{
headerLabelKey: 'comment.create.header',
buttonKey: 'global.action.save',
accentColor: 'info'
},
];
}
// Merge dialog config with finding data
dialogOptions = CommentDialogService.addDataToDialogConfig(config, dialogData);
return this.dialog.open(CommentDialogComponent, dialogOptions).onClose;
}
}

View File

@ -4,168 +4,167 @@
</nb-card-header> </nb-card-header>
<nb-card-body> <nb-card-body>
<form *ngIf="formArray" [formGroup]="findingFormGroup"> <form *ngIf="formArray" [formGroup]="findingFormGroup">
<div> <div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start"> <!-- Form Text Layout -->
<!-- Form Text Layout --> <div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start">
<div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start"> <!-- Title Form Field -->
<!-- Title Form Field --> <nb-form-field class="finding-form-field">
<nb-form-field class="finding-form-field"> <label for="{{formArray[0].fieldName}}" class="label">
<label for="{{formArray[0].fieldName}}" class="label"> {{formArray[0].labelKey | translate}}
{{formArray[0].labelKey | translate}} </label>
</label> <input formControlName="{{formArray[0].fieldName}}"
<input formControlName="{{formArray[0].fieldName}}" type="formText" required fullWidth
type="formText" required fullWidth id="{{formArray[0].fieldName}}" nbInput
id="{{formArray[0].fieldName}}" nbInput class="form-field form-text"
class="form-field form-text" [status]="findingFormGroup.get(formArray[0].fieldName).dirty ? (findingFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
[status]="findingFormGroup.get(formArray[0].fieldName).dirty ? (findingFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'" placeholder="{{formArray[0].placeholder | translate}} *">
placeholder="{{formArray[0].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 --> <ng-template ngFor let-error [ngForOf]="formArray[0].errors"
<ng-template ngFor let-error [ngForOf]="formArray[0].errors" *ngIf="findingFormGroup.get(formArray[0].fieldName).dirty">
*ngIf="findingFormGroup.get(formArray[0].fieldName).dirty">
<span class="error-text" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </nb-form-field>
<!-- Description Form Field --> <!-- Description Form Field -->
<nb-form-field class="finding-form-field"> <nb-form-field class="finding-form-field">
<label for="{{formArray[2].fieldName}}" class="label"> <label for="{{formArray[2].fieldName}}" class="label">
{{formArray[2].labelKey | translate}} {{formArray[2].labelKey | translate}}
</label> </label>
<textarea formControlName="{{formArray[2].fieldName}}" <textarea formControlName="{{formArray[2].fieldName}}"
type="formText" required fullWidth type="formText" required fullWidth
id="{{formArray[2].fieldName}}" nbInput id="{{formArray[2].fieldName}}" nbInput
class="form-field form-text" class="form-field form-text"
[status]="findingFormGroup.get(formArray[2].fieldName).dirty ? (findingFormGroup.get(formArray[2].fieldName).invalid ? 'danger' : 'basic') : 'basic'" [status]="findingFormGroup.get(formArray[2].fieldName).dirty ? (findingFormGroup.get(formArray[2].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{formArray[2].placeholder | translate}} *"> placeholder="{{formArray[2].placeholder | translate}} *">
</textarea> </textarea>
<!-- 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 -->
<ng-template ngFor let-error [ngForOf]="formArray[2].errors" <ng-template ngFor let-error [ngForOf]="formArray[2].errors"
*ngIf="findingFormGroup.get(formArray[2].fieldName).dirty"> *ngIf="findingFormGroup.get(formArray[2].fieldName).dirty">
<span class="error-text" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[2].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[2].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </nb-form-field>
<!-- Impact Form Field --> <!-- Impact Form Field -->
<nb-form-field class="finding-form-field"> <nb-form-field class="finding-form-field">
<label for="{{formArray[3].fieldName}}" class="label"> <label for="{{formArray[3].fieldName}}" class="label">
{{formArray[3].labelKey | translate}} {{formArray[3].labelKey | translate}}
</label> </label>
<textarea formControlName="{{formArray[3].fieldName}}" <textarea formControlName="{{formArray[3].fieldName}}"
type="formText" required fullWidth type="formText" required fullWidth
id="{{formArray[3].fieldName}}" nbInput id="{{formArray[3].fieldName}}" nbInput
class="form-field form-text" class="form-field form-text"
[status]="findingFormGroup.get(formArray[3].fieldName).dirty ? (findingFormGroup.get(formArray[3].fieldName).invalid ? 'danger' : 'basic') : 'basic'" [status]="findingFormGroup.get(formArray[3].fieldName).dirty ? (findingFormGroup.get(formArray[3].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{formArray[3].placeholder | translate}} *"> placeholder="{{formArray[3].placeholder | translate}} *">
</textarea> </textarea>
<!-- 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 -->
<ng-template ngFor let-error [ngForOf]="formArray[3].errors" <ng-template ngFor let-error [ngForOf]="formArray[3].errors"
*ngIf="findingFormGroup.get(formArray[3].fieldName).dirty"> *ngIf="findingFormGroup.get(formArray[3].fieldName).dirty">
<span class="error-text" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[3].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[3].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </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}} *"
type="severity-select"
[(selected)]="formArray[1].controlsConfig[0].value"
shape="round" 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> </div>
<!-- Affected URLs Layout --> <!-- Severity Layout -->
<!-- Affected URLs Form Field --> <!-- Severity Form Field -->
<nb-form-field class="finding-form-field"> <div fxFlex class="severity-dialog">
<label for="{{formArray[4].fieldName}}" class="label"> <label for="{{formArray[1].fieldName}}" class="label">
{{formArray[4].labelKey | translate}} {{formArray[1].labelKey | translate}}
</label> </label>
<input formControlName="{{formArray[4].fieldName}}" <nb-select class="severities" placeholder="{{formArray[1].placeholder | translate}} *"
type="text" type="severity-select"
id="{{formArray[4].fieldName}}" [(selected)]="formArray[1].controlsConfig[0].value"
nbTagInput fullWidth shape="round" status="{{getSeverityFillStatus(formArray[1].controlsConfig[0].value)}}" filled>
shape="rectangle" <nb-option *ngFor="let severity of severityTexts" [value]="severity.value">
(tagAdd)="onAffectedUrlAdd()" {{ severity.translationText | translate }}
class="form-field additionalUrl" </nb-option>
[status]="findingFormGroup.get(formArray[4].fieldName).dirty ? (findingFormGroup.get(formArray[4].fieldName).invalid ? 'danger' : 'basic') : 'basic'" </nb-select>
placeholder="{{formArray[4].placeholder | translate}}"> </div>
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed --> </div>
<ng-template ngFor let-error [ngForOf]="formArray[4].errors" <!-- Affected URLs Layout -->
*ngIf="findingFormGroup.get(formArray[4].fieldName).dirty"> <!-- 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" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[4].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[4].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </nb-form-field>
<!-- Add Affected URLs Button --> <!-- Add Affected URLs Button -->
<button nbButton status="primary" size="small" class="add-url-button" <button nbButton status="primary" size="small" class="add-url-button"
(click)="onAffectedUrlAdd()"> (click)="onAffectedUrlAdd()">
<fa-icon [icon]="fa.faPlus" class="new-url-icon"></fa-icon> <fa-icon [icon]="fa.faPlus" class="new-url-icon"></fa-icon>
<span> {{ 'finding.add.url' | translate }} </span> <span> {{ 'finding.add.url' | translate }} </span>
</button> </button>
<!----> <!---->
<nb-tag-list (tagRemove)="onAffectedUrlTagRemove($event)" class="url-tag-list"> <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 status="info" appearance="outline" class="url-tag" removable *ngFor="let url of affectedUrls"
</nb-tag-list> [text]="url"></nb-tag>
<!-- Additional Text Layout --> </nb-tag-list>
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start"> <!-- Additional Text Layout -->
<!-- Reproduction Form Field --> <div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
<nb-form-field fxFlex="50" class="finding-form-field"> <!-- Reproduction Form Field -->
<label for="{{formArray[5].fieldName}}" class="label"> <nb-form-field fxFlex="50" class="finding-form-field">
{{formArray[5].labelKey | translate}} <label for="{{formArray[5].fieldName}}" class="label">
</label> {{formArray[5].labelKey | translate}}
<textarea formControlName="{{formArray[5].fieldName}}" </label>
type="text" required fullWidth <textarea formControlName="{{formArray[5].fieldName}}"
id="{{formArray[5].fieldName}}" nbInput type="text" required fullWidth
class="form-field form-textarea" id="{{formArray[5].fieldName}}" nbInput
[status]="findingFormGroup.get(formArray[5].fieldName).dirty ? (findingFormGroup.get(formArray[5].fieldName).invalid ? 'danger' : 'basic') : 'basic'" class="form-field form-textarea"
placeholder="{{formArray[5].placeholder | translate}} *"> [status]="findingFormGroup.get(formArray[5].fieldName).dirty ? (findingFormGroup.get(formArray[5].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{formArray[5].placeholder | translate}} *">
</textarea> </textarea>
<!-- 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 -->
<ng-template ngFor let-error [ngForOf]="formArray[5].errors" <ng-template ngFor let-error [ngForOf]="formArray[5].errors"
*ngIf="findingFormGroup.get(formArray[5].fieldName).dirty"> *ngIf="findingFormGroup.get(formArray[5].fieldName).dirty">
<span class="error-text" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[5].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[5].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </nb-form-field>
<!-- Mitigation Form Field --> <!-- Mitigation Form Field -->
<nb-form-field fxFlex class="finding-form-field"> <nb-form-field fxFlex class="finding-form-field">
<label for="{{formArray[6].fieldName}}" class="label"> <label for="{{formArray[6].fieldName}}" class="label">
{{formArray[6].labelKey | translate}} {{formArray[6].labelKey | translate}}
</label> </label>
<textarea formControlName="{{formArray[6].fieldName}}" <textarea formControlName="{{formArray[6].fieldName}}"
type="text" fullWidth type="text" fullWidth
id="{{formArray[6].fieldName}}" nbInput id="{{formArray[6].fieldName}}" nbInput
class="form-field form-textarea" class="form-field form-textarea"
[status]="findingFormGroup.get(formArray[6].fieldName).dirty ? (findingFormGroup.get(formArray[6].fieldName).invalid ? 'danger' : 'basic') : 'basic'" [status]="findingFormGroup.get(formArray[6].fieldName).dirty ? (findingFormGroup.get(formArray[6].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{formArray[6].placeholder | translate}}"> placeholder="{{formArray[6].placeholder | translate}}">
</textarea> </textarea>
<!-- 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 -->
<ng-template ngFor let-error [ngForOf]="formArray[6].errors" <ng-template ngFor let-error [ngForOf]="formArray[6].errors"
*ngIf="findingFormGroup.get(formArray[6].fieldName).dirty"> *ngIf="findingFormGroup.get(formArray[6].fieldName).dirty">
<span class="error-text" <span class="error-text"
*ngIf="findingFormGroup.get(formArray[6].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'"> *ngIf="findingFormGroup.get(formArray[6].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}} {{error.translationKey | translate}}
</span> </span>
</ng-template> </ng-template>
</nb-form-field> </nb-form-field>
</div>
</div> </div>
</form> </form>
</nb-card-body> </nb-card-body>

View File

@ -96,13 +96,13 @@ describe('FindingDialogComponent', () => {
{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: mockedFindingDialogData} {provide: NB_DIALOG_CONFIG, useValue: mockedCommentDialogData}
] ]
}).compileComponents(); }).compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedFindingDialogData}); TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData});
fixture = TestBed.createComponent(FindingDialogComponent); fixture = TestBed.createComponent(FindingDialogComponent);
store = TestBed.inject(Store); store = TestBed.inject(Store);
store.reset({ store.reset({
@ -137,7 +137,7 @@ export const mockFinding: Finding = {
mitigation: 'Mitigation Test' mitigation: 'Mitigation Test'
}; };
export const mockedFindingDialogData = { export const mockedCommentDialogData = {
form: { form: {
findingTitle: { findingTitle: {
fieldName: 'findingTitle', fieldName: 'findingTitle',

View File

@ -6,7 +6,6 @@ import deepEqual from 'deep-equal';
import {UntilDestroy} from '@ngneat/until-destroy'; import {UntilDestroy} from '@ngneat/until-destroy';
import {Severity} from '@shared/models/severity.enum'; import {Severity} from '@shared/models/severity.enum';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {BehaviorSubject} from 'rxjs';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({

View File

@ -1,12 +1,9 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import { import {
NbButtonModule, NbButtonModule,
NbCardModule, NbCardModule,
NbDialogModule,
NbDialogService,
NbFormFieldModule, NbFormFieldModule,
NbInputModule, NbInputModule,
NbSelectModule, NbSelectModule,

View File

@ -0,0 +1,26 @@
import { TestBed } from '@angular/core/testing';
import { CommentService } from './comment.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
describe('CommentService', () => {
let service: CommentService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
NgxsModule.forRoot([ProjectState])
]
});
service = TestBed.inject(CommentService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient} from '@angular/common/http';
import {Store} from '@ngxs/store';
import {Observable} from 'rxjs';
import {Comment} from '@shared/models/comment.model';
@Injectable({
providedIn: 'root'
})
export class CommentService {
private apiBaseURL = `${environment.apiEndpoint}/pentests`;
constructor(
private http: HttpClient,
private readonly store: Store) {
}
/**
* Get Comments for Pentest Id
* @param pentestId the id of the project
*/
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
}
/**
* Save Comment
* @param pentestId the id of the pentest
* @param comment the information of the comment
*/
public saveComment(pentestId: string, comment: Comment): Observable<Comment> {
return this.http.post<Comment>(`${this.apiBaseURL}/${pentestId}/comment`, comment);
}
}

View File

@ -121,28 +121,4 @@ export class PentestService {
public deleteFindingByPentestAndFindingId(pentestId: string, findingId: string): Observable<string> { public deleteFindingByPentestAndFindingId(pentestId: string, findingId: string): Observable<string> {
return this.http.delete<string>(`${this.apiBaseURL}/${pentestId}/finding/${findingId}`); return this.http.delete<string>(`${this.apiBaseURL}/${pentestId}/finding/${findingId}`);
} }
/**
* Get Comments for Pentest Id
* @param pentestId the id of the project
*/
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
// return of([]);
/* ToDo: Use mocked Comments?
return of([
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd2',
title: 'This is a creative title',
description: 'This is a creative description',
relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'],
},
{
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
title: 'This is a creative title',
description: 'This is a creative description',
relatedFindings: [],
}
]);*/
}
} }

View File

@ -40,3 +40,10 @@ export class UpdatePentestFindings {
constructor(public findingId: string) { constructor(public findingId: string) {
} }
} }
export class UpdatePentestComments {
static readonly type = '[ProjectState] UpdatePentestComments';
constructor(public commentId: string) {
}
}

View File

@ -5,7 +5,7 @@ import {
ChangeCategory, ChangeCategory,
ChangePentest, ChangePentest,
ChangeProject, ChangeProject,
InitProjectState, InitProjectState, UpdatePentestComments,
UpdatePentestFindings UpdatePentestFindings
} from '@shared/stores/project-state/project-state.actions'; } from '@shared/stores/project-state/project-state.actions';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
@ -108,4 +108,26 @@ export class ProjectState {
selectedPentest: stateSelectedPentest selectedPentest: stateSelectedPentest
}); });
} }
@Action(UpdatePentestComments)
updatePentestComments(ctx: StateContext<ProjectStateModel>, {commentId}: UpdatePentestComments): void {
const state = ctx.getState();
let stateSelectedPentest: Pentest = state.selectedPentest;
const stateCommentIds: Array<string> = stateSelectedPentest.commentIds || [];
let updatedCommentIds: Array<string> = [];
if (!stateCommentIds.includes(commentId)) {
updatedCommentIds = [...stateCommentIds, commentId];
} else {
// ToDo: Add logic to remove commentId from array
}
// overwrites only findingIds
stateSelectedPentest = {
...stateSelectedPentest,
commentIds: updatedCommentIds
};
// path project state
ctx.patchState({
selectedPentest: stateSelectedPentest
});
}
} }

View File

@ -457,6 +457,90 @@
} }
] ]
}, },
{
"name": "comments",
"item": [
{
"name": "saveComment",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw",
"type": "string"
},
{
"key": "undefined",
"type": "any"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"affectedUrls\": []\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/comment",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"pentests",
"11601f51-bc17-47fd-847d-0c53df5405b5",
"comment"
]
}
},
"response": []
},
{
"name": "getCommentsForPentesId",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw",
"type": "string"
},
{
"key": "undefined",
"type": "any"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/comments",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"pentests",
"11601f51-bc17-47fd-847d-0c53df5405b5",
"comments"
]
}
},
"response": []
}
]
},
{ {
"name": "getPentestsByProjectIdAndCategory", "name": "getPentestsByProjectIdAndCategory",
"request": { "request": {

View File

@ -255,10 +255,54 @@ include::{snippets}/deleteFindingByPentestAndFindingId/http-response.adoc[]
include::{snippets}/deleteFindingByPentestAndFindingId/response-fields.adoc[] include::{snippets}/deleteFindingByPentestAndFindingId/response-fields.adoc[]
== Comment
=== Get comments for pentest
To get comments by pentestId, call the GET request /pentests/+{pentestId}+/comments.
==== Request example
include::{snippets}/getCommentsByPentestId/http-request.adoc[]
==== Request structure
include::{snippets}/getCommentsByPentestId/path-parameters.adoc[]
==== Response example
include::{snippets}/getCommentsByPentestId/http-response.adoc[]
==== Response structure
include::{snippets}/getCommentsByPentestId/response-fields.adoc[]
=== Save comment
To save a comment, call the POST request /pentests/+{pentestId}+/comment
==== Request example
include::{snippets}/saveCommentByPentestId/http-request.adoc[]
==== Request structure
include::{snippets}/saveCommentByPentestId/path-parameters.adoc[]
==== Response example
include::{snippets}/saveCommentByPentestId/http-response.adoc[]
==== Response structure
include::{snippets}/saveCommentByPentestId/response-fields.adoc[]
== Change History == Change History
|=== |===
|Date |Change |Date |Change
|2022-12-22
|Added GET, POST endpoint for Comment
|2022-12-09 |2022-12-09
|Added DELETE endpoint for Finding |Added DELETE endpoint for Finding
|2022-12-08 |2022-12-08

View File

@ -1,5 +1,7 @@
package com.securityc4po.api.comment package com.securityc4po.api.comment
import com.securityc4po.api.ResponseBody
import com.securityc4po.api.finding.FindingRequestBody
import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.index.Indexed
import java.util.* import java.util.*
@ -10,3 +12,55 @@ data class Comment (
val description: String, val description: String,
val relatedFindings: List<String>? = emptyList() val relatedFindings: List<String>? = emptyList()
) )
fun buildComment(body: CommentRequestBody, commentEntity: CommentEntity): Comment {
return Comment(
id = commentEntity.data.id,
title = body.title,
description = body.description,
relatedFindings = body.relatedFindings
)
}
data class CommentRequestBody(
val title: String,
val description: String,
val relatedFindings: List<String>? = emptyList()
)
fun Comment.toCommentResponseBody(): ResponseBody {
return mapOf(
"id" to id,
"title" to title,
"description" to description,
"relatedFindings" to relatedFindings
)
}
fun Comment.toCommentDeleteResponseBody(): ResponseBody {
return mapOf(
"id" to id
)
}
/**
* Validates if a [FindingRequestBody] is valid
*
* @return Boolean describing if the body is valid
*/
fun CommentRequestBody.isValid(): Boolean {
return when {
this.title.isBlank() -> false
this.description.isBlank() -> false
else -> true
}
}
fun CommentRequestBody.toComment(): Comment {
return Comment(
id = UUID.randomUUID().toString(),
title = this.title,
description = this.description,
relatedFindings = this.relatedFindings
)
}

View File

@ -0,0 +1,51 @@
package com.securityc4po.api.comment
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import com.securityc4po.api.ResponseBody
import com.securityc4po.api.configuration.error.handler.EntityNotFoundException
import com.securityc4po.api.configuration.error.handler.Errorcode
import com.securityc4po.api.finding.toFindingResponseBody
import com.securityc4po.api.pentest.PentestService
import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.noContent
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
@RestController
@RequestMapping("/pentests")
@CrossOrigin(
origins = [],
allowCredentials = "false",
allowedHeaders = ["*"],
methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PATCH]
)
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
class CommentController(private val pentestService: PentestService, private val commentService: CommentService) {
var logger = getLoggerFor<CommentController>()
@GetMapping("/{pentestId}/comments")
fun getComments(@PathVariable(value = "pentestId") pentestId: String): Mono<ResponseEntity<List<ResponseBody>>> {
return this.pentestService.getCommentIdsByPentestId(pentestId).flatMap { commentIds: List<String> ->
this.commentService.getCommentsByIds(commentIds).map { commentList ->
commentList.map { it.toCommentResponseBody() }
}
}.map {
if (it.isEmpty()) noContent().build()
else ResponseEntity.ok(it)
}
}
@PostMapping("/{pentestId}/comment")
fun saveComment(
@PathVariable(value = "pentestId") pentestId: String,
@RequestBody body: CommentRequestBody
): Mono<ResponseEntity<ResponseBody>> {
return this.commentService.saveComment(pentestId, body).map {
ResponseEntity.accepted().body(it.toCommentResponseBody())
}
}
}

View File

@ -0,0 +1,21 @@
package com.securityc4po.api.comment
import org.springframework.data.mongodb.repository.DeleteQuery
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Repository
interface CommentRepository : ReactiveMongoRepository<CommentEntity, String> {
@Query("{'data._id' : ?0}")
fun findCommentById(id: String): Mono<CommentEntity>
@Query("{'data._id' :{\$in: ?0 }}")
fun findCommentsByIds(id: List<String>): Flux<CommentEntity>
@DeleteQuery("{'data._id' : ?0}")
fun deleteCommentById(id: String): Mono<Long>
}

View File

@ -0,0 +1,77 @@
package com.securityc4po.api.comment
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.error.handler.*
import com.securityc4po.api.configuration.error.handler.EntityNotFoundException
import com.securityc4po.api.configuration.error.handler.InvalidModelException
import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException
import com.securityc4po.api.extensions.getLoggerFor
import com.securityc4po.api.finding.*
import com.securityc4po.api.pentest.PentestService
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
@Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
class CommentService(private val commentRepository: CommentRepository, private val pentestService: PentestService) {
var logger = getLoggerFor<CommentService>()
/**
* Save [Finding]
*
* @throws [InvalidModelException] if the [Finding] is invalid
* @throws [TransactionInterruptedException] if the [Finding] could not be stored
* @return saved [Finding]
*/
fun saveComment(pentestId: String, body: CommentRequestBody): Mono<Comment> {
validate(
require = body.isValid(),
logging = { logger.warn("Comment not valid.") },
mappedException = InvalidModelException(
"Comment not valid.", Errorcode.CommentInvalid
)
)
val comment = body.toComment()
val commentEntity = CommentEntity(comment)
return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity ->
val finding = newCommentEntity.toComment()
// After successfully saving finding add id to pentest
pentestService.updatePentestComment(pentestId, comment.id).onErrorMap {
TransactionInterruptedException(
"Pentest could not be updated in Database.",
Errorcode.PentestInsertionFailed
)
}.map {
finding
}
}.doOnError {
throw wrappedException(
logging = { logger.warn("Comment could not be stored in Database. Thrown exception: ", it) },
mappedException = TransactionInterruptedException(
"Comment could not be stored.",
Errorcode.CommentInsertionFailed
)
)
}
}
/**
* Get all [Comments]s by commentId's
*
* @return list of [Comment]s
*/
fun getCommentsByIds(commentIds: List<String>): Mono<List<Comment>> {
return commentRepository.findCommentsByIds(commentIds).collectList().map {
it.map { commentEntity -> commentEntity.toComment() }
}.switchIfEmpty {
val msg = "Comment not found."
val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound)
logger.warn(msg, ex)
throw ex
}
}
}

View File

@ -7,6 +7,8 @@ enum class Errorcode(val code: Int) {
PentestNotFound(1003), PentestNotFound(1003),
FindingsNotFound(1004), FindingsNotFound(1004),
FindingNotFound(1005), FindingNotFound(1005),
CommentsNotFound(1006),
CommentNotFound(1007),
// 2XXX Already Changed // 2XXX Already Changed
ProjectAlreadyChanged(2001), ProjectAlreadyChanged(2001),
@ -20,6 +22,7 @@ enum class Errorcode(val code: Int) {
TokenWithoutField(3004), TokenWithoutField(3004),
UserIdIsEmpty(3005), UserIdIsEmpty(3005),
FindingInvalid(3006), FindingInvalid(3006),
CommentInvalid(3007),
// 4XXX Unauthorized // 4XXX Unauthorized
ProjectAdjustmentNotAuthorized(4000), ProjectAdjustmentNotAuthorized(4000),
@ -40,4 +43,6 @@ enum class Errorcode(val code: Int) {
ProjectPentestInsertionFailed(6008), ProjectPentestInsertionFailed(6008),
FindingInsertionFailed(6009), FindingInsertionFailed(6009),
FindingDeletionFailed(6010), FindingDeletionFailed(6010),
CommentInsertionFailed(6011),
CommentDeletionFailed(6012),
} }

View File

@ -12,7 +12,7 @@ data class Pentest(
val refNumber: String, val refNumber: String,
val status: PentestStatus, val status: PentestStatus,
var findingIds: List<String> = emptyList(), var findingIds: List<String> = emptyList(),
val commentIds: List<String> = emptyList() var commentIds: List<String> = emptyList()
) )
fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest { fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest {

View File

@ -79,7 +79,7 @@ class PentestController(private val pentestService: PentestService, private val
findingList.map { it.toFindingResponseBody() } findingList.map { it.toFindingResponseBody() }
} }
}.map { }.map {
if (it.isEmpty()) ResponseEntity.noContent().build() if (it.isEmpty()) noContent().build()
else ResponseEntity.ok(it) else ResponseEntity.ok(it)
} }
} }
@ -119,7 +119,7 @@ class PentestController(private val pentestService: PentestService, private val
return this.findingService.deleteFindingByPentestAndFindingId(pentestId, findingId).map { return this.findingService.deleteFindingByPentestAndFindingId(pentestId, findingId).map {
ResponseEntity.ok().body(it.toFindingDeleteResponseBody()) ResponseEntity.ok().body(it.toFindingDeleteResponseBody())
}.switchIfEmpty { }.switchIfEmpty {
Mono.just(ResponseEntity.noContent().build<ResponseBody>()) Mono.just(noContent().build<ResponseBody>())
} }
} }
} }

View File

@ -201,4 +201,50 @@ class PentestService(private val pentestRepository: PentestRepository, private v
throw ex throw ex
}.map { pentestEntity -> pentestEntity.data.findingIds } }.map { pentestEntity -> pentestEntity.data.findingIds }
} }
/**
* Update [Pentest] for Comment
*
* @throws [InvalidModelException] if the [Pentest] is invalid
* @throws [TransactionInterruptedException] if the [Pentest] could not be updated
* @return updated [Pentest]
*/
fun updatePentestComment(pentestId: String, commentId: String): Mono<Pentest> {
return pentestRepository.findPentestById(pentestId).switchIfEmpty {
logger.warn("Pentest with id $pentestId not found. Updating not possible.")
val msg = "Pentest with id $pentestId not found."
val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
throw ex
}.flatMap { currentPentestEntity: PentestEntity ->
if (currentPentestEntity.data.commentIds.find { pentestData -> pentestData == commentId } == null) {
currentPentestEntity.data.commentIds += commentId
}
currentPentestEntity.lastModified = Instant.now()
this.pentestRepository.save(currentPentestEntity).map {
it.toPentest()
}.doOnError {
throw wrappedException(
logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
mappedException = TransactionInterruptedException(
"Pentest could not be updated.",
Errorcode.PentestInsertionFailed
)
)
}
}
}
/**
* Get all [Comment]Id's by pentestId
*
* @return list of [String]
*/
fun getCommentIdsByPentestId(pentestId: String): Mono<List<String>> {
return this.pentestRepository.findPentestById(pentestId).switchIfEmpty {
logger.warn("Pentest with id $pentestId not found. Collecting comments not possible.")
val msg = "Pentest with id $pentestId not found."
val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
throw ex
}.map { pentestEntity -> pentestEntity.data.commentIds }
}
} }

View File

@ -0,0 +1,218 @@
package com.securityc4po.api.comment
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.common.Json
import com.securityc4po.api.BaseDocumentationIntTest
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
import com.securityc4po.api.pentest.Pentest
import com.securityc4po.api.pentest.PentestCategory
import com.securityc4po.api.pentest.PentestEntity
import com.securityc4po.api.pentest.PentestStatus
import com.securityc4po.api.project.Project
import com.securityc4po.api.project.ProjectEntity
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.restdocs.operation.preprocess.Preprocessors
import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation
import org.springframework.restdocs.request.RequestDocumentation
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
import reactor.core.publisher.Mono
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
@Autowired
lateinit var mongoTemplate: MongoTemplate
var mapper = ObjectMapper()
@BeforeEach
fun init() {
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetComments {
@Test
fun getCommentsByPentestId() {
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
webTestClient.get()
.uri("/pentests/{pentestId}/comments", pentestTwoId)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().doesNotExist("")
.expectBody().json(Json.write(getCommentsResponse()))
.consumeWith(
WebTestClientRestDocumentation.document(
"{methodName}",
Preprocessors.preprocessRequest(
Preprocessors.prettyPrint(),
Preprocessors.modifyUris().removePort(),
Preprocessors.removeHeaders("Host", "Content-Length")
),
Preprocessors.preprocessResponse(
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to get the comments for")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING)
.description("The id of the requested comment"),
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("[].description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("[].relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related Findings of the comment")
)
)
)
}
private val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
private fun getCommentsResponse() = listOf(
commentOne.toCommentResponseBody()
)
}
@Nested
inner class SaveComment {
@Test
fun saveCommentByPentestId() {
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
webTestClient.post()
.uri("/pentests/{pentestId}/comment", pentestTwoId)
.header("Authorization", "Bearer $tokenAdmin")
.body(Mono.just(commentBody), CommentRequestBody::class.java)
.exchange()
.expectStatus().isAccepted
.expectHeader().doesNotExist("")
.expectBody().json(Json.write(commentBody))
.consumeWith(
WebTestClientRestDocumentation.document(
"{methodName}",
Preprocessors.preprocessRequest(
Preprocessors.prettyPrint(),
Preprocessors.modifyUris().removePort(),
Preprocessors.removeHeaders("Host", "Content-Length")
),
Preprocessors.preprocessResponse(
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to save the comment for")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
.description("The id of the saved comment"),
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
)
)
)
}
private val commentBody = CommentRequestBody(
title = "Found another Bug",
description = "Another OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
}
private fun persistBasicTestScenario() {
// setup test data
// Project
val projectOne = Project(
id = "d2e126ba-f608-11ec-b939-0242ac120025",
client = "E Corp",
title = "Some Mock API (v1.0) Scanning",
createdAt = "2021-01-10T18:05:00Z",
tester = "Novatester",
projectPentests = emptyList(),
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
// Pentests
val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.INFORMATION_GATHERING,
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = emptyList(),
commentIds = emptyList()
)
val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.INFORMATION_GATHERING,
refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(),
commentIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f")
)
val pentestThree = Pentest(
id = "74eae112-f62c-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.AUTHENTICATION_TESTING,
refNumber = "OTG-AUTHN-001",
status = PentestStatus.COMPLETED,
findingIds = emptyList(),
commentIds = emptyList()
)
// Comment
val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))
mongoTemplate.save(PentestEntity(pentestOne))
mongoTemplate.save(PentestEntity(pentestTwo))
mongoTemplate.save(PentestEntity(pentestThree))
mongoTemplate.save(CommentEntity(commentOne))
}
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
mongoTemplate.findAllAndRemove(Query(), CommentEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -0,0 +1,178 @@
package com.securityc4po.api.comment
import com.github.tomakehurst.wiremock.common.Json
import com.securityc4po.api.BaseIntTest
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
import com.securityc4po.api.pentest.Pentest
import com.securityc4po.api.pentest.PentestCategory
import com.securityc4po.api.pentest.PentestEntity
import com.securityc4po.api.pentest.PentestStatus
import com.securityc4po.api.project.Project
import com.securityc4po.api.project.ProjectEntity
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.test.web.reactive.server.WebTestClient
import reactor.core.publisher.Mono
import java.time.Duration
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class CommentControllerIntegrationTest: BaseIntTest() {
@LocalServerPort
private var port = 0
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
private lateinit var webTestClient: WebTestClient
@BeforeEach
fun setupWebClient() {
webTestClient = WebTestClient.bindToServer()
.baseUrl("http://localhost:$port")
.responseTimeout(Duration.ofMillis(10000))
.build()
}
@BeforeEach
fun init() {
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetComments {
@Test
fun `requesting comments by pentestId successfully`() {
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
webTestClient.get()
.uri("/pentests/{pentestId}/comments", pentestTwoId)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody().json(Json.write(getComments()))
}
private val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
private fun getComments() = listOf(
commentOne.toCommentResponseBody()
)
}
@Nested
inner class SaveComment {
@Test
fun `save comment successfully`() {
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
webTestClient.post()
.uri("/pentests/{pentestId}/comment", pentestTwoId)
.header("Authorization", "Bearer $tokenAdmin")
.body(Mono.just(commentBody), CommentRequestBody::class.java)
.exchange()
.expectStatus().isAccepted
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody()
.jsonPath("$.title").isEqualTo("Found another Bug")
.jsonPath("$.description").isEqualTo("Another OTG-INFO-002 Bug")
.jsonPath("$.relatedFindings").isEmpty
}
private val commentBody = CommentRequestBody(
title = "Found another Bug",
description = "Another OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
}
private fun persistBasicTestScenario() {
// setup test data
// project
val projectOne = Project(
id = "d2e126ba-f608-11ec-b939-0242ac120025",
client = "E Corp",
title = "Some Mock API (v1.0) Scanning",
createdAt = "2021-01-10T18:05:00Z",
tester = "Novatester",
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
)
// pentests
val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.INFORMATION_GATHERING,
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = emptyList(),
commentIds = emptyList()
)
val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.INFORMATION_GATHERING,
refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(),
commentIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f")
)
val pentestThree = Pentest(
id = "16vbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
category = PentestCategory.AUTHENTICATION_TESTING,
refNumber = "OTG-AUTHN-001",
status = PentestStatus.COMPLETED,
findingIds = emptyList(),
commentIds = emptyList()
)
// Comment
val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))
mongoTemplate.save(PentestEntity(pentestOne))
mongoTemplate.save(PentestEntity(pentestTwo))
mongoTemplate.save(PentestEntity(pentestThree))
mongoTemplate.save(CommentEntity(commentOne))
}
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
mongoTemplate.findAllAndRemove(Query(), CommentEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -390,7 +390,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
.description("The severity of the finding"), .description("The severity of the finding"),
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the requested finding"), .description("The title of the finding"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description number of the finding"), .description("The description number of the finding"),
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
@ -554,7 +554,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
findingIds = emptyList(), findingIds = emptyList(),
commentIds = emptyList() commentIds = emptyList()
) )
// Findings // Finding
val findingOne = Finding( val findingOne = Finding(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f", id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
severity = Severity.LOW, severity = Severity.LOW,

View File

@ -0,0 +1,54 @@
[{
"_id": {
"$oid": "63a4530b9bb2aa2c3bc77b52"
},
"lastModified": {
"$date": {
"$numberLong": "1671713547508"
}
},
"data": {
"_id": "89703b19-16c7-49e5-8e33-0c706313e5fe",
"title": "Test Comment",
"description": "Test Comment Description",
"relatedFindings": []
},
"_class": "com.securityc4po.api.comment.CommentEntity"
},{
"_id": {
"$oid": "63a453e4377c3f53f86d27d8"
},
"lastModified": {
"$date": {
"$numberLong": "1671713764781"
}
},
"data": {
"_id": "df516de6-ca5e-44a6-ac50-db89bb17aac3",
"title": "New Test",
"description": "New Test",
"relatedFindings": [
"0bda8950-94fa-4ec6-8fa7-e09f5a8cd3e8",
"4ddb84f6-068c-4319-a8ee-1000008bb75a"
]
},
"_class": "com.securityc4po.api.comment.CommentEntity"
},{
"_id": {
"$oid": "63a454b5377c3f53f86d27d9"
},
"lastModified": {
"$date": {
"$numberLong": "1671713973541"
}
},
"data": {
"_id": "e55e943b-6a48-4d84-8d72-b48d7d9de5b7",
"title": "Wow another one?",
"description": "Epic!",
"relatedFindings": [
"5e22d38f-a4f6-4809-84ea-a803b5f1f9fc"
]
},
"_class": "com.securityc4po.api.comment.CommentEntity"
}]

View File

@ -4,7 +4,7 @@
}, },
"lastModified": { "lastModified": {
"$date": { "$date": {
"$numberLong": "1668425376074" "$numberLong": "1671713973546"
} }
}, },
"data": { "data": {
@ -14,9 +14,22 @@
"refNumber": "OTG-INFO-001", "refNumber": "OTG-INFO-001",
"status": "IN_PROGRESS", "status": "IN_PROGRESS",
"findingIds": [ "findingIds": [
"ef31449d-71ec-4736-952f-8b20e53117d5" "ef31449d-71ec-4736-952f-8b20e53117d5",
"0bda8950-94fa-4ec6-8fa7-e09f5a8cd3e8",
"58f63b4e-97fb-4fe8-8527-7996896089d2",
"72886128-b2d9-4a92-bbfe-b54373441321",
"4ddb84f6-068c-4319-a8ee-1000008bb75a",
"42831151-51fd-4348-b829-6b18ddd14fe1",
"559cd0ac-9e64-41f9-892a-4c8a9dd30357",
"5e22d38f-a4f6-4809-84ea-a803b5f1f9fc",
"0bfa7511-fe33-4ab5-9af2-d4ed70c1b350",
"70e413b9-d736-40d2-b7d6-236768b1230c"
], ],
"commentIds": [] "commentIds": [
"89703b19-16c7-49e5-8e33-0c706313e5fe",
"df516de6-ca5e-44a6-ac50-db89bb17aac3",
"e55e943b-6a48-4d84-8d72-b48d7d9de5b7"
]
}, },
"_class": "com.securityc4po.api.pentest.PentestEntity" "_class": "com.securityc4po.api.pentest.PentestEntity"
},{ },{
@ -1387,4 +1400,202 @@
"commentIds": [] "commentIds": []
}, },
"_class": "com.securityc4po.api.pentest.PentestEntity" "_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374dd53e0136563b96187c1"
},
"lastModified": {
"$date": {
"$numberLong": "1668603470508"
}
},
"data": {
"_id": "617cc86d-aaa5-4144-93b6-1799e913ba21",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "INFORMATION_GATHERING",
"refNumber": "OTG-INFO-001",
"status": "OPEN",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374de60e0136563b96187c2"
},
"lastModified": {
"$date": {
"$numberLong": "1668603488283"
}
},
"data": {
"_id": "58c47263-47b8-4fd5-804d-092dad0a0cbb",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "CONFIGURATION_AND_DEPLOY_MANAGEMENT_TESTING",
"refNumber": "OTG-CONFIG-001",
"status": "IN_PROGRESS",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374e698e0136563b96187c3"
},
"lastModified": {
"$date": {
"$numberLong": "1668605592100"
}
},
"data": {
"_id": "34ccdaf4-0449-478c-b621-80c3e6417256",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "AUTHORIZATION_TESTING",
"refNumber": "OTG-AUTHZ-001",
"status": "IN_PROGRESS",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374e77fe0136563b96187c4"
},
"lastModified": {
"$date": {
"$numberLong": "1668605823136"
}
},
"data": {
"_id": "f1c3b1be-e509-48ac-aefd-5ef406f4adaf",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "SESSION_MANAGEMENT_TESTING",
"refNumber": "OTG-SESS-001",
"status": "IN_PROGRESS",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374e7d8e0136563b96187c5"
},
"lastModified": {
"$date": {
"$numberLong": "1668605912794"
}
},
"data": {
"_id": "a276d02c-28b4-4354-9fda-98280809a17c",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "INPUT_VALIDATION_TESTING",
"refNumber": "OTG-INPVAL-001",
"status": "IN_PROGRESS",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374e931e0136563b96187c6"
},
"lastModified": {
"$date": {
"$numberLong": "1668606263295"
}
},
"data": {
"_id": "fd26e674-9eea-478b-a82c-36c6cf8e37f9",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "CRYPTOGRAPHY",
"refNumber": "OTG-CRYPST-001",
"status": "IN_PROGRESS",
"findingIds": [],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6374ec24e0136563b96187c7"
},
"lastModified": {
"$date": {
"$numberLong": "1668607029078"
}
},
"data": {
"_id": "29bc612f-c093-40f8-8ebd-e03c9775194d",
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
"category": "BUSINESS_LOGIC_TESTING",
"refNumber": "OTG-BUSLOGIC-001",
"status": "IN_PROGRESS",
"findingIds": [
"672d9f87-fb3d-4fc5-8c6f-cadf97661ca5"
],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "6376047e0687d905ca60af1c"
},
"lastModified": {
"$date": {
"$numberLong": "1668679299852"
}
},
"data": {
"_id": "33c90b03-e475-407f-b58f-64833aa58480",
"projectId": "ecece991-d3e0-4089-84ef-730bac197fbf",
"category": "INPUT_VALIDATION_TESTING",
"refNumber": "OTG-INPVAL-005",
"status": "IN_PROGRESS",
"findingIds": [
"bddf810b-f20e-473e-a63d-34fcba7e48ef"
],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "637611070687d905ca60af1f"
},
"lastModified": {
"$date": {
"$numberLong": "1668682319332"
}
},
"data": {
"_id": "4ed11922-0b11-4134-a616-0da781d9ad77",
"projectId": "e86984d1-6ed4-4cb7-aa17-d8d4d7c23f61",
"category": "CLIENT_SIDE_TESTING",
"refNumber": "OTG-CLIENT-001",
"status": "IN_PROGRESS",
"findingIds": [
"d7c95af7-5434-4768-b62c-5b11f9396276"
],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
},{
"_id": {
"$oid": "63776766fcdda12bf2e51eb1"
},
"lastModified": {
"$date": {
"$numberLong": "1670579952464"
}
},
"data": {
"_id": "52c9ce82-b4f7-48d9-b92d-4895d04ffdf8",
"projectId": "9217fd68-3fad-478b-a900-b05ed549bef2",
"category": "CLIENT_SIDE_TESTING",
"refNumber": "OTG-CLIENT-001",
"status": "IN_PROGRESS",
"findingIds": [
"cb33fad4-7965-4654-a9f9-f007edaca35c"
],
"commentIds": []
},
"_class": "com.securityc4po.api.pentest.PentestEntity"
}] }]