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,
|
FindingDialogBody,
|
||||||
FindingEntry,
|
FindingEntry,
|
||||||
transformFindingsToObjectiveEntries,
|
transformFindingsToObjectiveEntries,
|
||||||
transformFindingToRequestBody
|
transformFindingToRequestBody,
|
||||||
} from '@shared/models/finding.model';
|
} from '@shared/models/finding.model';
|
||||||
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
||||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -111,7 +111,7 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
).pipe(
|
).pipe(
|
||||||
filter(value => !!value),
|
filter(value => !!value),
|
||||||
/* tap((value) => console.warn('FindingDialogBody: ', value))*/
|
/*tap((value) => console.warn('FindingDialogBody: ', value)),*/
|
||||||
mergeMap((value: FindingDialogBody) =>
|
mergeMap((value: FindingDialogBody) =>
|
||||||
this.pentestService.saveFinding(
|
this.pentestService.saveFinding(
|
||||||
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
|
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
|
||||||
|
@ -120,8 +120,8 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
),
|
),
|
||||||
untilDestroyed(this)
|
untilDestroyed(this)
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (finding) => {
|
next: (newFinding: Finding) => {
|
||||||
this.store.dispatch(new UpdatePentestFindings(finding.id));
|
this.store.dispatch(new UpdatePentestFindings(newFinding.id));
|
||||||
this.loadFindingsData();
|
this.loadFindingsData();
|
||||||
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
|
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
|
||||||
},
|
},
|
||||||
|
@ -132,12 +132,55 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickEditFinding(finding): void {
|
onClickEditFinding(findingEntry): void {
|
||||||
console.info('Coming soon..');
|
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 {
|
onClickDeleteFinding(findingEntry): void {
|
||||||
console.info('Coming soon..');
|
console.info('Coming soon..', findingEntry.data.findingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading(): Observable<boolean> {
|
isLoading(): Observable<boolean> {
|
||||||
|
|
|
@ -134,7 +134,8 @@
|
||||||
"update.success": "Fund erfolgreich aktualisiert",
|
"update.success": "Fund erfolgreich aktualisiert",
|
||||||
"update.failed": "Fund konnte nicht aktualisiert werden",
|
"update.failed": "Fund konnte nicht aktualisiert werden",
|
||||||
"delete.success": "Fund erfolgreich gelöscht",
|
"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": {
|
"severities": {
|
||||||
|
|
|
@ -134,7 +134,8 @@
|
||||||
"update.success": "Finding updated successfully",
|
"update.success": "Finding updated successfully",
|
||||||
"update.failed": "Finding could not be updated",
|
"update.failed": "Finding could not be updated",
|
||||||
"delete.success": "Finding deleted successfully",
|
"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": {
|
"severities": {
|
||||||
|
|
|
@ -6,6 +6,7 @@ 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({
|
||||||
|
@ -33,6 +34,7 @@ export class FindingDialogComponent implements OnInit {
|
||||||
|
|
||||||
// ToDo: Adjust for edit finding dialog to include existing urls
|
// ToDo: Adjust for edit finding dialog to include existing urls
|
||||||
affectedUrls: string[] = [];
|
affectedUrls: string[] = [];
|
||||||
|
initialAffectedUrls: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
||||||
|
@ -44,6 +46,9 @@ export class FindingDialogComponent implements OnInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.findingFormGroup = this.generateFormCreationFieldArray();
|
this.findingFormGroup = this.generateFormCreationFieldArray();
|
||||||
this.dialogData = this.data;
|
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 {
|
generateFormCreationFieldArray(): FormGroup {
|
||||||
|
@ -52,6 +57,11 @@ export class FindingDialogComponent implements OnInit {
|
||||||
...accumulator,
|
...accumulator,
|
||||||
[currentValue?.fieldName]: currentValue?.controlsConfig
|
[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);
|
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 {
|
onAffectedUrlAdd(): void {
|
||||||
// tslint:disable-next-line:no-string-literal
|
// tslint:disable-next-line:no-string-literal
|
||||||
const newUrl = this.findingFormGroup.controls['findingAffectedUrls'].value;
|
const newUrl = this.findingFormGroup.controls['findingAffectedUrls'].value;
|
||||||
|
@ -127,11 +142,12 @@ export class FindingDialogComponent implements OnInit {
|
||||||
const newFindingData = this.findingFormGroup.getRawValue();
|
const newFindingData = this.findingFormGroup.getRawValue();
|
||||||
Object.entries(newFindingData).forEach(entry => {
|
Object.entries(newFindingData).forEach(entry => {
|
||||||
const [key, value] = 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] = '';
|
newFindingData[key] = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const didChange = !deepEqual(oldFindingData, newFindingData);
|
const didChange = !deepEqual(oldFindingData, newFindingData) || !deepEqual(this.initialAffectedUrls, this.affectedUrls);
|
||||||
return didChange;
|
return didChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +159,13 @@ export class FindingDialogComponent implements OnInit {
|
||||||
const findingData = {};
|
const findingData = {};
|
||||||
Object.entries(dialogData.form).forEach(entry => {
|
Object.entries(dialogData.form).forEach(entry => {
|
||||||
const [key, value] = entry;
|
const [key, value] = entry;
|
||||||
|
// console.info(key);
|
||||||
findingData[key] = value.controlsConfig[0] ?
|
findingData[key] = value.controlsConfig[0] ?
|
||||||
(value.controlsConfig[0].value ? value.controlsConfig[0].value : 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;
|
return findingData;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,11 @@ export class FindingDialogService {
|
||||||
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
||||||
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
||||||
let dialogData: GenericDialogData;
|
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
|
// Setup FindingDialogBody
|
||||||
dialogData = {
|
dialogData = {
|
||||||
form: {
|
form: {
|
||||||
|
@ -56,7 +61,7 @@ export class FindingDialogService {
|
||||||
labelKey: 'finding.severity.label',
|
labelKey: 'finding.severity.label',
|
||||||
placeholder: 'finding.severity',
|
placeholder: 'finding.severity',
|
||||||
controlsConfig: [
|
controlsConfig: [
|
||||||
{value: finding ? finding.severity : Severity.LOW, disabled: false},
|
{value: finding ? severity : Severity.LOW, disabled: false},
|
||||||
[Validators.required]
|
[Validators.required]
|
||||||
],
|
],
|
||||||
errors: [
|
errors: [
|
||||||
|
@ -95,7 +100,7 @@ export class FindingDialogService {
|
||||||
labelKey: 'finding.affectedUrls.label',
|
labelKey: 'finding.affectedUrls.label',
|
||||||
placeholder: 'finding.affectedUrls.placeholder',
|
placeholder: 'finding.affectedUrls.placeholder',
|
||||||
controlsConfig: [
|
controlsConfig: [
|
||||||
{value: '', disabled: false},
|
{value: finding ? finding.affectedUrls : [], disabled: false},
|
||||||
[]
|
[]
|
||||||
],
|
],
|
||||||
errors: [
|
errors: [
|
||||||
|
|
|
@ -85,42 +85,14 @@ export class PentestService {
|
||||||
*/
|
*/
|
||||||
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
|
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
|
||||||
return this.http.get<Finding[]>(`${this.apiBaseURL}/${pentestId}/findings`);
|
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);
|
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
|
* Get Comments for Pentest Id
|
||||||
* @param pentestId the id of the project
|
* @param pentestId the id of the project
|
||||||
|
|
|
@ -339,6 +339,85 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response": []
|
"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[]
|
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
|
=== Save finding
|
||||||
|
|
||||||
To save a finding, call the POST request /pentests/+{pentestId}+/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[]
|
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
|
== Change History
|
||||||
|
|
||||||
|===
|
|===
|
||||||
|Date |Change
|
|Date |Change
|
||||||
|
|2022-12-09
|
||||||
|
|Added GET and PATCH endpoint for Finding
|
||||||
|2022-12-02
|
|2022-12-02
|
||||||
|Added GET and POST endpoint for Findings
|
|Added GET and POST endpoint for Findings
|
||||||
|2022-11-21
|
|2022-11-21
|
||||||
|
|
|
@ -16,6 +16,19 @@ data class Finding (
|
||||||
val mitigation: String?
|
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(
|
data class FindingRequestBody(
|
||||||
val severity: String,
|
val severity: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
|
@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
|
* 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")
|
@PostMapping("/{pentestId}/finding")
|
||||||
fun saveFinding(
|
fun saveFinding(
|
||||||
@PathVariable(value = "pentestId") pentestId: String,
|
@PathVariable(value = "pentestId") pentestId: String,
|
||||||
|
@ -91,4 +98,14 @@ class PentestController(private val pentestService: PentestService, private val
|
||||||
ResponseEntity.accepted().body(it.toFindingResponseBody())
|
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
|
@Nested
|
||||||
inner class SaveFinding {
|
inner class SaveFinding {
|
||||||
@Test
|
@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() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
// Project
|
// Project
|
||||||
|
|
|
@ -190,7 +190,33 @@ class PentestControllerIntTest : BaseIntTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@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
|
@Test
|
||||||
fun `save finding successfully`() {
|
fun `save finding successfully`() {
|
||||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
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() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
// project
|
// project
|
||||||
|
|
Loading…
Reference in New Issue