feat: As a user I want to edit my finding
This commit is contained in:
parent
076fa087e8
commit
27a8e963e9
|
@ -10,7 +10,7 @@ import {
|
|||
FindingDialogBody,
|
||||
FindingEntry,
|
||||
transformFindingsToObjectiveEntries,
|
||||
transformFindingToRequestBody
|
||||
transformFindingToRequestBody,
|
||||
} from '@shared/models/finding.model';
|
||||
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
|
@ -111,7 +111,7 @@ export class PentestFindingsComponent implements OnInit {
|
|||
}
|
||||
).pipe(
|
||||
filter(value => !!value),
|
||||
/* tap((value) => console.warn('FindingDialogBody: ', value))*/
|
||||
/*tap((value) => console.warn('FindingDialogBody: ', value)),*/
|
||||
mergeMap((value: FindingDialogBody) =>
|
||||
this.pentestService.saveFinding(
|
||||
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
|
||||
|
@ -120,8 +120,8 @@ export class PentestFindingsComponent implements OnInit {
|
|||
),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (finding) => {
|
||||
this.store.dispatch(new UpdatePentestFindings(finding.id));
|
||||
next: (newFinding: Finding) => {
|
||||
this.store.dispatch(new UpdatePentestFindings(newFinding.id));
|
||||
this.loadFindingsData();
|
||||
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
|
||||
},
|
||||
|
@ -132,12 +132,55 @@ export class PentestFindingsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onClickEditFinding(finding): void {
|
||||
console.info('Coming soon..');
|
||||
onClickEditFinding(findingEntry): void {
|
||||
this.pentestService.getFindingById(findingEntry.data.findingId).pipe(
|
||||
filter(isNotNullOrUndefined),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (existingFinding: Finding) => {
|
||||
if (existingFinding) {
|
||||
this.findingDialogService.openFindingDialog(
|
||||
FindingDialogComponent,
|
||||
existingFinding,
|
||||
{
|
||||
closeOnEsc: false,
|
||||
hasScroll: false,
|
||||
autoFocus: false,
|
||||
closeOnBackdropClick: false
|
||||
}
|
||||
).pipe(
|
||||
filter(value => !!value),
|
||||
/*tap((value) => console.warn('FindingDialogBody: ', value)),*/
|
||||
mergeMap((value: FindingDialogBody) =>
|
||||
this.pentestService.updateFinding(
|
||||
findingEntry.data.findingId,
|
||||
transformFindingToRequestBody(value)
|
||||
)
|
||||
),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (updatedFinding: Finding) => {
|
||||
this.store.dispatch(new UpdatePentestFindings(updatedFinding.id));
|
||||
this.loadFindingsData();
|
||||
this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS);
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
this.notificationService.showPopup('finding.popup.update.failed', PopupType.FAILURE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.notificationService.showPopup('finding.popup.not.available', PopupType.FAILURE);
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClickDeleteFinding(finding): void {
|
||||
console.info('Coming soon..');
|
||||
onClickDeleteFinding(findingEntry): void {
|
||||
console.info('Coming soon..', findingEntry.data.findingId);
|
||||
}
|
||||
|
||||
isLoading(): Observable<boolean> {
|
||||
|
|
|
@ -134,7 +134,8 @@
|
|||
"update.success": "Fund erfolgreich aktualisiert",
|
||||
"update.failed": "Fund konnte nicht aktualisiert werden",
|
||||
"delete.success": "Fund erfolgreich gelöscht",
|
||||
"delete.failed": "Fund konnte nicht gelöscht werden"
|
||||
"delete.failed": "Fund konnte nicht gelöscht werden",
|
||||
"not.available": "Fund ist nicht mehr verfügbar"
|
||||
}
|
||||
},
|
||||
"severities": {
|
||||
|
|
|
@ -134,7 +134,8 @@
|
|||
"update.success": "Finding updated successfully",
|
||||
"update.failed": "Finding could not be updated",
|
||||
"delete.success": "Finding deleted successfully",
|
||||
"delete.failed": "Finding could not be deleted"
|
||||
"delete.failed": "Finding could not be deleted",
|
||||
"not.available": "Finding is not available anymore"
|
||||
}
|
||||
},
|
||||
"severities": {
|
||||
|
|
|
@ -6,6 +6,7 @@ import deepEqual from 'deep-equal';
|
|||
import {UntilDestroy} from '@ngneat/until-destroy';
|
||||
import {Severity} from '@shared/models/severity.enum';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -33,6 +34,7 @@ export class FindingDialogComponent implements OnInit {
|
|||
|
||||
// ToDo: Adjust for edit finding dialog to include existing urls
|
||||
affectedUrls: string[] = [];
|
||||
initialAffectedUrls: string[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
||||
|
@ -44,6 +46,9 @@ export class FindingDialogComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.findingFormGroup = this.generateFormCreationFieldArray();
|
||||
this.dialogData = this.data;
|
||||
// Resets affected Urls input fields when finding was found in dialog context
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
this.findingFormGroup.controls['findingAffectedUrls'].reset('');
|
||||
}
|
||||
|
||||
generateFormCreationFieldArray(): FormGroup {
|
||||
|
@ -52,6 +57,11 @@ export class FindingDialogComponent implements OnInit {
|
|||
...accumulator,
|
||||
[currentValue?.fieldName]: currentValue?.controlsConfig
|
||||
}), {});
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
const affectedUrls = this.data.form['findingAffectedUrls'].controlsConfig[0].value;
|
||||
if (affectedUrls) {
|
||||
this.renderAffectedUrls(affectedUrls);
|
||||
}
|
||||
return this.fb.group(config);
|
||||
}
|
||||
|
||||
|
@ -67,6 +77,11 @@ export class FindingDialogComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
renderAffectedUrls(affectedUrls: string[]): void {
|
||||
affectedUrls.forEach(url => this.initialAffectedUrls.push(url));
|
||||
affectedUrls.forEach(url => this.affectedUrls.push(url));
|
||||
}
|
||||
|
||||
onAffectedUrlAdd(): void {
|
||||
// tslint:disable-next-line:no-string-literal
|
||||
const newUrl = this.findingFormGroup.controls['findingAffectedUrls'].value;
|
||||
|
@ -127,11 +142,12 @@ export class FindingDialogComponent implements OnInit {
|
|||
const newFindingData = this.findingFormGroup.getRawValue();
|
||||
Object.entries(newFindingData).forEach(entry => {
|
||||
const [key, value] = entry;
|
||||
if (value === null) {
|
||||
// Affected Url form field can be ignored since changes here will be recognised inside affectedUrls of tag-list
|
||||
if (value === null || key === 'findingAffectedUrls') {
|
||||
newFindingData[key] = '';
|
||||
}
|
||||
});
|
||||
const didChange = !deepEqual(oldFindingData, newFindingData);
|
||||
const didChange = !deepEqual(oldFindingData, newFindingData) || !deepEqual(this.initialAffectedUrls, this.affectedUrls);
|
||||
return didChange;
|
||||
}
|
||||
|
||||
|
@ -143,8 +159,13 @@ export class FindingDialogComponent implements OnInit {
|
|||
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]) : '';
|
||||
// Affected Url form field can be ignored since changes here will be recognised inside affectedUrls of tag-list
|
||||
if (key === 'findingAffectedUrls') {
|
||||
findingData[key] = '';
|
||||
}
|
||||
});
|
||||
return findingData;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,11 @@ export class FindingDialogService {
|
|||
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
||||
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
||||
let dialogData: GenericDialogData;
|
||||
let severity;
|
||||
// transform severity of finding if existing
|
||||
if (finding) {
|
||||
severity = typeof finding.severity !== 'number' ? Severity[finding.severity] : finding.severity;
|
||||
}
|
||||
// Setup FindingDialogBody
|
||||
dialogData = {
|
||||
form: {
|
||||
|
@ -56,7 +61,7 @@ export class FindingDialogService {
|
|||
labelKey: 'finding.severity.label',
|
||||
placeholder: 'finding.severity',
|
||||
controlsConfig: [
|
||||
{value: finding ? finding.severity : Severity.LOW, disabled: false},
|
||||
{value: finding ? severity : Severity.LOW, disabled: false},
|
||||
[Validators.required]
|
||||
],
|
||||
errors: [
|
||||
|
@ -95,7 +100,7 @@ export class FindingDialogService {
|
|||
labelKey: 'finding.affectedUrls.label',
|
||||
placeholder: 'finding.affectedUrls.placeholder',
|
||||
controlsConfig: [
|
||||
{value: '', disabled: false},
|
||||
{value: finding ? finding.affectedUrls : [], disabled: false},
|
||||
[]
|
||||
],
|
||||
errors: [
|
||||
|
|
|
@ -85,42 +85,14 @@ export class PentestService {
|
|||
*/
|
||||
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
|
||||
return this.http.get<Finding[]>(`${this.apiBaseURL}/${pentestId}/findings`);
|
||||
// return of([]);
|
||||
/*Todo: Remove mocked Findings?
|
||||
return of([
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||
title: 'This is a creative title',
|
||||
description: 'test',
|
||||
impact: 'This impacts only the UI',
|
||||
severity: Severity.LOW,
|
||||
reproduction: ''
|
||||
},
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||
title: 'This is a creative title',
|
||||
description: 'test',
|
||||
impact: 'This is impacts some things',
|
||||
severity: Severity.MEDIUM,
|
||||
reproduction: ''
|
||||
},
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||
title: 'This is a creative title',
|
||||
description: 'test',
|
||||
impact: 'This is impacts a lot',
|
||||
severity: Severity.HIGH,
|
||||
reproduction: ''
|
||||
},
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||
title: 'This is a creative title',
|
||||
description: 'test',
|
||||
impact: 'This is impacts a lot',
|
||||
severity: Severity.CRITICAL,
|
||||
reproduction: ''
|
||||
}
|
||||
]);*/
|
||||
|
||||
/**
|
||||
* Get Finding by Id
|
||||
* @param findingId the id of the finding
|
||||
*/
|
||||
public getFindingById(findingId: string): Observable<Finding> {
|
||||
return this.http.get<Finding>(`${this.apiBaseURL}/${findingId}/finding`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,6 +104,15 @@ export class PentestService {
|
|||
return this.http.post<Finding>(`${this.apiBaseURL}/${pentestId}/finding`, finding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Finding
|
||||
* @param findingId the id of the finding
|
||||
* @param finding the information of the finding
|
||||
*/
|
||||
public updateFinding(findingId: string, finding: Finding): Observable<Finding> {
|
||||
return this.http.patch<Finding>(`${this.apiBaseURL}/${findingId}/finding`, finding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Comments for Pentest Id
|
||||
* @param pentestId the id of the project
|
||||
|
|
|
@ -339,6 +339,85 @@
|
|||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "getFindingById",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "undefined",
|
||||
"type": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/pentests/cb33fad4-7965-4654-a9f9-f007edaca35c/finding",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"pentests",
|
||||
"cb33fad4-7965-4654-a9f9-f007edaca35c",
|
||||
"finding"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "updateFinding",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "undefined",
|
||||
"type": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "PATCH",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"title\": \"Test Title\",\n \"severity\": \"CRITICAL\",\n \"description\": \"Test Description\",\n \"impact\": \"Test Impact\",\n \"affectedUrls\": [\n \"https://akveo.github.io/nebular/docs/components/progress-bar/examples#nbprogressbarcomponent\"\n ],\n \"reproduction\": \"Step 1: Test\",\n \"mitigation\": \"Test Mitigatin\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/pentests/cb33fad4-7965-4654-a9f9-f007edaca35c/finding",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"pentests",
|
||||
"cb33fad4-7965-4654-a9f9-f007edaca35c",
|
||||
"finding"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -175,6 +175,26 @@ include::{snippets}/getFindingsByPentestId/http-response.adoc[]
|
|||
|
||||
include::{snippets}/getFindingsByPentestId/response-fields.adoc[]
|
||||
|
||||
=== Get finding for id
|
||||
|
||||
To get a finding by id, call the GET request /pentests/+{findingId}+/finding.
|
||||
|
||||
==== Request example
|
||||
|
||||
include::{snippets}/getFindingById/http-request.adoc[]
|
||||
|
||||
==== Request structure
|
||||
|
||||
include::{snippets}/getFindingById/path-parameters.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
include::{snippets}/getFindingById/http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
include::{snippets}/getFindingById/response-fields.adoc[]
|
||||
|
||||
=== Save finding
|
||||
|
||||
To save a finding, call the POST request /pentests/+{pentestId}+/finding
|
||||
|
@ -195,10 +215,32 @@ include::{snippets}/saveFindingByPentestId/http-response.adoc[]
|
|||
|
||||
include::{snippets}/saveFindingByPentestId/response-fields.adoc[]
|
||||
|
||||
=== Update finding
|
||||
|
||||
To update a finding, call the PATCH request /pentests/+{findingId}+/finding
|
||||
|
||||
==== Request example
|
||||
|
||||
include::{snippets}/updateFindingById/http-request.adoc[]
|
||||
|
||||
==== Request structure
|
||||
|
||||
include::{snippets}/updateFindingById/path-parameters.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
include::{snippets}/updateFindingById/http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
include::{snippets}/updateFindingById/response-fields.adoc[]
|
||||
|
||||
== Change History
|
||||
|
||||
|===
|
||||
|Date |Change
|
||||
|2022-12-09
|
||||
|Added GET and PATCH endpoint for Finding
|
||||
|2022-12-02
|
||||
|Added GET and POST endpoint for Findings
|
||||
|2022-11-21
|
||||
|
|
|
@ -16,6 +16,19 @@ data class Finding (
|
|||
val mitigation: String?
|
||||
)
|
||||
|
||||
fun buildFinding(body: FindingRequestBody, findingEntity: FindingEntity): Finding {
|
||||
return Finding(
|
||||
id = findingEntity.data.id,
|
||||
severity = Severity.valueOf(body.severity),
|
||||
title = body.title,
|
||||
description = body.description,
|
||||
impact = body.impact,
|
||||
affectedUrls = body.affectedUrls,
|
||||
reproduction = body.reproduction,
|
||||
mitigation = body.mitigation
|
||||
)
|
||||
}
|
||||
|
||||
data class FindingRequestBody(
|
||||
val severity: String,
|
||||
val title: String,
|
||||
|
|
|
@ -11,6 +11,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
|||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
|
||||
|
@ -57,6 +58,43 @@ class FindingService(private val findingRepository: FindingRepository, private v
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update [Finding]
|
||||
*
|
||||
* @throws [InvalidModelException] if the [Finding] is invalid
|
||||
* @throws [TransactionInterruptedException] if the [Finding] could not be stored
|
||||
* @return saved [Finding]
|
||||
*/
|
||||
fun updateFinding(findingId: String, body: FindingRequestBody): Mono<Finding> {
|
||||
validate(
|
||||
require = body.isValid(),
|
||||
logging = { logger.warn("Finding not valid.") },
|
||||
mappedException = InvalidModelException(
|
||||
"Finding not valid.", Errorcode.FindingInvalid
|
||||
)
|
||||
)
|
||||
return this.findingRepository.findFindingById(findingId).switchIfEmpty {
|
||||
logger.warn("Finding with id $findingId not found.")
|
||||
val msg = "Finding with id $findingId not found."
|
||||
val ex = EntityNotFoundException(msg, Errorcode.FindingNotFound)
|
||||
throw ex
|
||||
}.flatMap { currentFindingEntity: FindingEntity ->
|
||||
currentFindingEntity.lastModified = Instant.now()
|
||||
currentFindingEntity.data = buildFinding(body, currentFindingEntity)
|
||||
findingRepository.save(currentFindingEntity).map {
|
||||
it.toFinding()
|
||||
}
|
||||
}.doOnError {
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Finding could not be updated in Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Finding could not be stored.",
|
||||
Errorcode.FindingInsertionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get [Finding] by findingId
|
||||
*
|
||||
|
|
|
@ -82,6 +82,13 @@ class PentestController(private val pentestService: PentestService, private val
|
|||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{findingId}/finding")
|
||||
fun getFinding(@PathVariable(value = "findingId") findingId: String):Mono<ResponseEntity<ResponseBody>> {
|
||||
return this.findingService.getFindingById(findingId).map {
|
||||
ResponseEntity.ok().body(it.toFindingResponseBody())
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{pentestId}/finding")
|
||||
fun saveFinding(
|
||||
@PathVariable(value = "pentestId") pentestId: String,
|
||||
|
@ -91,4 +98,14 @@ class PentestController(private val pentestService: PentestService, private val
|
|||
ResponseEntity.accepted().body(it.toFindingResponseBody())
|
||||
}
|
||||
}
|
||||
|
||||
@PatchMapping("/{findingId}/finding")
|
||||
fun updateFinding(
|
||||
@PathVariable(value = "findingId") findingId: String,
|
||||
@RequestBody body: FindingRequestBody
|
||||
): Mono<ResponseEntity<ResponseBody>> {
|
||||
return this.findingService.updateFinding(findingId, body).map {
|
||||
ResponseEntity.accepted().body(it.toFindingResponseBody())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -297,6 +297,66 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetFinding {
|
||||
@Test
|
||||
fun getFindingById() {
|
||||
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
|
||||
webTestClient.get()
|
||||
.uri("/pentests/{findingId}/finding", findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().doesNotExist("")
|
||||
.expectBody().json(Json.write(findingOne.toFindingResponseBody()))
|
||||
.consumeWith(
|
||||
WebTestClientRestDocumentation.document(
|
||||
"{methodName}",
|
||||
Preprocessors.preprocessRequest(
|
||||
Preprocessors.prettyPrint(),
|
||||
Preprocessors.modifyUris().removePort(),
|
||||
Preprocessors.removeHeaders("Host", "Content-Length")
|
||||
),
|
||||
Preprocessors.preprocessResponse(
|
||||
Preprocessors.prettyPrint()
|
||||
),
|
||||
RequestDocumentation.relaxedPathParameters(
|
||||
RequestDocumentation.parameterWithName("findingId").description("The id of the feinidng you want to get")
|
||||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
.description("The title of the requested finding"),
|
||||
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
|
||||
.description("The description number of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
|
||||
.description("The impact of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY)
|
||||
.description("List of affected Urls of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING)
|
||||
.description("The reproduction steps of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING)
|
||||
.description("The example mitigation for the finding")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val findingOne = Finding(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
severity = Severity.LOW,
|
||||
title = "Found Bug",
|
||||
description = "OTG-INFO-002 Bug",
|
||||
impact = "Service",
|
||||
affectedUrls = emptyList(),
|
||||
reproduction = "Step 1: Hack",
|
||||
mitigation = "None"
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SaveFinding {
|
||||
@Test
|
||||
|
@ -357,6 +417,66 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class UpdateFinding {
|
||||
@Test
|
||||
fun updateFindingById() {
|
||||
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
|
||||
webTestClient.patch()
|
||||
.uri("/pentests/{findingId}/finding", findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.body(Mono.just(findingBody), FindingRequestBody::class.java)
|
||||
.exchange()
|
||||
.expectStatus().isAccepted
|
||||
.expectHeader().doesNotExist("")
|
||||
.expectBody().json(Json.write(findingBody))
|
||||
.consumeWith(
|
||||
WebTestClientRestDocumentation.document(
|
||||
"{methodName}",
|
||||
Preprocessors.preprocessRequest(
|
||||
Preprocessors.prettyPrint(),
|
||||
Preprocessors.modifyUris().removePort(),
|
||||
Preprocessors.removeHeaders("Host", "Content-Length")
|
||||
),
|
||||
Preprocessors.preprocessResponse(
|
||||
Preprocessors.prettyPrint()
|
||||
),
|
||||
RequestDocumentation.relaxedPathParameters(
|
||||
RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to update")
|
||||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
.description("The title of the requested finding"),
|
||||
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
|
||||
.description("The description number of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
|
||||
.description("The impact of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY)
|
||||
.description("List of affected Urls of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING)
|
||||
.description("The reproduction steps of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING)
|
||||
.description("The example mitigation for the finding")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val findingBody = FindingRequestBody(
|
||||
severity = "HIGH",
|
||||
title = "Updated Bug",
|
||||
description = "Updated OTG-INFO-002 Bug",
|
||||
impact = "Service",
|
||||
affectedUrls = emptyList(),
|
||||
reproduction = "Step 1: Hack",
|
||||
mitigation = "Still None"
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// Project
|
||||
|
|
|
@ -190,7 +190,33 @@ class PentestControllerIntTest : BaseIntTest() {
|
|||
}
|
||||
|
||||
@Nested
|
||||
inner class SaveFindings {
|
||||
inner class GetFinding {
|
||||
@Test
|
||||
fun `requesting finding by findingId successfully`() {
|
||||
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
|
||||
webTestClient.get()
|
||||
.uri("/pentests/{findingId}/finding", findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||
.expectBody().json(Json.write(findingOne.toFindingResponseBody()))
|
||||
}
|
||||
|
||||
private val findingOne = Finding(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
severity = Severity.LOW,
|
||||
title = "Found Bug",
|
||||
description = "OTG-INFO-002 Bug",
|
||||
impact = "Service",
|
||||
affectedUrls = emptyList(),
|
||||
reproduction = "Step 1: Hack",
|
||||
mitigation = "None"
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SaveFinding {
|
||||
@Test
|
||||
fun `save finding successfully`() {
|
||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
|
@ -222,6 +248,39 @@ class PentestControllerIntTest : BaseIntTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class UpdateFinding {
|
||||
@Test
|
||||
fun `update finding successfully`() {
|
||||
val findingId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
webTestClient.post()
|
||||
.uri("/pentests/{findingId}/finding", findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.body(Mono.just(findingBody), FindingRequestBody::class.java)
|
||||
.exchange()
|
||||
.expectStatus().isAccepted
|
||||
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||
.expectBody()
|
||||
.jsonPath("$.severity").isEqualTo("HIGH")
|
||||
.jsonPath("$.title").isEqualTo("Updated Bug")
|
||||
.jsonPath("$.description").isEqualTo("Updated OTG-INFO-002 Bug")
|
||||
.jsonPath("$.impact").isEqualTo("Service")
|
||||
.jsonPath("$.affectedUrls").isEmpty
|
||||
.jsonPath("$.reproduction").isEqualTo("Step 1: Hack")
|
||||
.jsonPath("$.mitigation").isEqualTo("Still None")
|
||||
}
|
||||
|
||||
private val findingBody = FindingRequestBody(
|
||||
severity = "HIGH",
|
||||
title = "Updated Bug",
|
||||
description = "Updated OTG-INFO-002 Bug",
|
||||
impact = "Service",
|
||||
affectedUrls = emptyList(),
|
||||
reproduction = "Step 1: Hack",
|
||||
mitigation = "Still None"
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// project
|
||||
|
|
Loading…
Reference in New Issue