fix: Bugfixes and QoL Improvements

This commit is contained in:
Marcel Haag 2022-11-16 12:49:53 +01:00
parent e9aec4ec3e
commit a4536e9735
24 changed files with 426 additions and 275 deletions

View File

@ -11,6 +11,15 @@
<div class="filler"></div> <div class="filler"></div>
<div fxLayoutGap="4rem"> <div fxLayoutGap="4rem">
<nb-actions size="medium"> <nb-actions size="medium">
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')">
<fa-icon [icon]="fa.faFileInvoice" class="new-element-icon" href="https://www.google.com">
</fa-icon>
<span class="owasp-redirect-button">OWASP</span>
</button>
</nb-action>
<nb-action class="toggle-theme"> <nb-action class="toggle-theme">
<button nbButton <button nbButton
(click)="onClickSwitchTheme()"> (click)="onClickSwitchTheme()">

View File

@ -8,6 +8,10 @@
flex-grow: 1; flex-grow: 1;
} }
.owasp-redirect-button {
margin-left: 0.5rem;
}
.languageContainer { .languageContainer {
display: flex; display: flex;
max-width: 8rem; max-width: 8rem;

View File

@ -33,6 +33,11 @@ export class HeaderComponent implements OnInit{
this.selectedLanguage = this.translateService.currentLang; this.selectedLanguage = this.translateService.currentLang;
} }
// HTML only
onClickGoToLink(url: string): void {
window.open(url, '_blank');
}
onClickSwitchTheme(): void { onClickSwitchTheme(): void {
if (this.currentTheme === 'corporate') { if (this.currentTheme === 'corporate') {
this.themeService.changeTheme('dark'); this.themeService.changeTheme('dark');

View File

@ -9,7 +9,8 @@
.export-button-container { .export-button-container {
display: flex; display: flex;
align-content: flex-end; align-content: flex-end;
margin-right: 0.5rem; // ToDo: Fix so that longer / shorter name won't change needed margin
margin-right: 2.25rem;
.export-element-icon { .export-element-icon {
} }

View File

@ -76,8 +76,24 @@ export class ObjectiveTableComponent implements OnInit {
}) })
).finally(); ).finally();
*/ */
const statePentest = this.pentests$.getValue().find(pentest => pentest.refNumber === selectedPentest.refNumber); const statePentest: Pentest = this.pentests$.getValue().find(pentest => pentest.refNumber === selectedPentest.refNumber);
this.store.dispatch(new ChangePentest(statePentest)); if (statePentest) {
this.store.dispatch(new ChangePentest(statePentest));
} else {
let childEntryStatePentest;
// ToDo: Fix wrong selection
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < this.pentests$.getValue().length; i++) {
if (this.pentests$.getValue()[i].childEntries) {
const findingResult = this.pentests$.getValue()[i].childEntries.find(cE => cE.refNumber === selectedPentest.refNumber);
if (findingResult) {
childEntryStatePentest = findingResult;
break;
}
}
}
this.store.dispatch(new ChangePentest(childEntryStatePentest));
}
} }
// HTML only // HTML only

View File

@ -49,6 +49,7 @@
size="small" size="small"
shape="round" shape="round"
class="add-comment-button" class="add-comment-button"
[disabled]="pentestInfo$.getValue().status === notStartedStatus"
(click)="onClickAddComment()"> (click)="onClickAddComment()">
<fa-icon [icon]="fa.faPlus" class="new-comment-icon"></fa-icon> <fa-icon [icon]="fa.faPlus" class="new-comment-icon"></fa-icon>
{{'comment.add' | translate}} {{'comment.add' | translate}}

View File

@ -6,6 +6,8 @@
.comment-cell { .comment-cell {
// Add style here // Add style here
height: 4.5rem !important;
max-height: 4.5rem !important;
} }
.comment-cell:hover { .comment-cell:hover {

View File

@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs'; 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';
@ -9,6 +9,9 @@ import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, tap} from 'rxjs/operators'; import {filter, tap} from 'rxjs/operators';
import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model'; import {Comment, CommentEntry, transformCommentsToObjectiveEntries} 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 {Store} from '@ngxs/store';
import {PentestStatus} from '@shared/models/pentest-status.model';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -18,11 +21,11 @@ import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
}) })
export class PentestCommentsComponent implements OnInit { export class PentestCommentsComponent implements OnInit {
@Input()
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
// HTML only // HTML only
readonly fa = FA; readonly fa = FA;
notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
// comments$: BehaviorSubject<Comment[]> = new BehaviorSubject<Comment[]>(null); // comments$: BehaviorSubject<Comment[]> = new BehaviorSubject<Comment[]>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
@ -41,24 +44,39 @@ export class PentestCommentsComponent implements OnInit {
constructor(private readonly pentestService: PentestService, constructor(private readonly pentestService: PentestService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>, private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
private store: Store,
private notificationService: NotificationService) { private notificationService: NotificationService) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters); this.dataSource = dataSourceBuilder.create(this.data, this.getters);
} }
ngOnInit(): void { ngOnInit(): void {
this.loadCommentsData(); this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
this.pentestInfo$.next(selectedPentest);
this.loadCommentsData();
},
error: err => {
console.error(err);
}
});
} }
loadCommentsData(): void { loadCommentsData(): void {
this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe( .pipe(
untilDestroyed(this), untilDestroyed(this),
filter(isNotNullOrUndefined), /*filter(isNotNullOrUndefined),*/
tap(() => this.loading$.next(true)) tap(() => this.loading$.next(true))
) )
.subscribe({ .subscribe({
next: (comments: Comment[]) => { next: (comments: Comment[]) => {
this.data = transformCommentsToObjectiveEntries(comments); if (comments) {
this.data = transformCommentsToObjectiveEntries(comments);
} else {
this.data = [];
}
this.dataSource.setData(this.data, this.getters); this.dataSource.setData(this.data, this.getters);
this.loading$.next(false); this.loading$.next(false);
}, },

View File

@ -2,52 +2,21 @@
<div class="content"> <div class="content">
<nb-tabset> <nb-tabset>
<nb-tab class="pentest-tabset" tabTitle="{{ 'global.action.info' | translate }}"> <nb-tab class="pentest-tabset" tabTitle="{{ 'global.action.info' | translate }}">
<app-pentest-info [pentestInfo$]=pentest$></app-pentest-info> <app-pentest-info></app-pentest-info>
</nb-tab> </nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.findings' | translate }}" <nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.findings' | translate }}"
badgeText="{{currentNumberOfFindings$.getValue()}}" badgeStatus="danger"> badgeText="{{currentNumberOfFindings$.getValue()}}" badgeStatus="danger">
<app-pentest-findings [pentestInfo$]=pentest$></app-pentest-findings> <app-pentest-findings></app-pentest-findings>
</nb-tab> </nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}" <nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}"
badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info"> badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info">
<app-pentest-comments [pentestInfo$]=pentest$></app-pentest-comments> <app-pentest-comments></app-pentest-comments>
</nb-tab> </nb-tab>
</nb-tabset> </nb-tabset>
</div> </div>
<div fxLayoutAlign="end end" class="content-footer"> <div fxLayoutAlign="end end" class="content-footer">
<!-- Pentest Status Selection --> <!--ToDo: Use to put element in bottom-right corner -->
<div class="pentest-status-dialog">
<nb-select class="status"
[(selected)]="currentStatus"
shape="round" filled
status="{{getPentestFillStatus(currentStatus)}}">
<nb-option *ngFor="let status of statusTexts" [value]="status.value">
{{ status.translationText | translate }}
</nb-option>
</nb-select>
</div>
<div *ngIf="!pentest$.getValue().id; else updatePentest">
<button nbButton
class="save-pentest-button"
status="primary"
[disabled]="!pentestStatusChanged()"
title="{{ 'global.action.save' | translate }}"
(click)="onClickSavePentest()">
<span class="exit-element-text"> {{ 'global.action.save' | translate }} </span>
</button>
</div>
<ng-template #updatePentest>
<button nbButton
class="save-pentest-button"
status="primary"
[disabled]="!pentestStatusChanged()"
title="{{ 'global.action.update' | translate }}"
(click)="onClickUpdatePentest()">
<span class="exit-element-text"> {{ 'global.action.update' | translate }} </span>
</button>
</ng-template>
</div> </div>
</div> </div>

View File

@ -10,33 +10,9 @@
/*nb-tab { /*nb-tab {
position: fixed; position: fixed;
}*/ }*/
}
.content-footer { .content-footer {
height: 5%; height: 5%;
.pentest-status-dialog {
margin: 1rem 2.25rem 1rem 0;
.status {
width: 12rem;
}
.basic {
background-color: nb-theme(color-basic-default);
}
.info {
background-color: nb-theme(color-info-default);
}
.warning {
background-color: nb-theme(color-warning-default);
}
.success {
background-color: nb-theme(color-success-default);
}
}
.save-pentest-button {
margin: 1rem 6rem 1rem 0; margin: 1rem 6rem 1rem 0;
} }
} }

View File

@ -1,17 +1,12 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
import {Select, Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {StatusText} from '@shared/widgets/status-tag/status-tag.component';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/notification.service';
import {Project} from '@shared/models/project.model';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {filter} from 'rxjs/operators';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -23,28 +18,10 @@ export class PentestContentComponent implements OnInit {
// HTML only // HTML only
readonly fa = FA; readonly fa = FA;
@Select(ProjectState.project)
selectedProject$: Observable<Project>;
selectedProjectId: string;
pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null); pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
pentestChanged$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
currentNumberOfFindings$: BehaviorSubject<number> = new BehaviorSubject<number>(0); currentNumberOfFindings$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
currentNumberOfComments$: BehaviorSubject<number> = new BehaviorSubject<number>(0); currentNumberOfComments$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
// Pentest Status Handler
currentStatus: PentestStatus = PentestStatus.NOT_STARTED;
private initialPentestStatus: PentestStatus;
status = PentestStatus;
readonly statusTexts: Array<StatusText> = [
{value: PentestStatus.NOT_STARTED, translationText: 'pentest.statusText.not_started'},
/* ToDo: Disabled not needed inside pentest */
/*{value: PentestStatus.DISABLED, translationText: 'pentest.statusText.disabled'},*/
{value: PentestStatus.OPEN, translationText: 'pentest.statusText.open'},
{value: PentestStatus.IN_PROGRESS, translationText: 'pentest.statusText.in_progress'},
{value: PentestStatus.COMPLETED, translationText: 'pentest.statusText.completed'}
];
constructor( constructor(
private readonly pentestService: PentestService, private readonly pentestService: PentestService,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -52,26 +29,11 @@ export class PentestContentComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.selectedProject$.pipe(
filter(isNotNullOrUndefined),
untilDestroyed(this)
).subscribe({
next: (project) => {
this.selectedProjectId = project.id;
},
error: (err) => {
console.error(err);
}
});
this.store.select(ProjectState.pentest).pipe( this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (selectedPentest: Pentest) => { next: (selectedPentest: Pentest) => {
console.warn(selectedPentest);
this.pentest$.next(selectedPentest); this.pentest$.next(selectedPentest);
this.currentStatus = selectedPentest.status;
this.initialPentestStatus = selectedPentest.status;
const findings = selectedPentest.findingIds ? selectedPentest.findingIds.length : 0; const findings = selectedPentest.findingIds ? selectedPentest.findingIds.length : 0;
this.currentNumberOfFindings$.next(findings); this.currentNumberOfFindings$.next(findings);
const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0; const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0;
@ -82,78 +44,4 @@ export class PentestContentComponent implements OnInit {
} }
}); });
} }
onClickSavePentest(): void {
this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus});
this.pentestService.savePentest(this.selectedProjectId, transformPentestToRequestBody(this.pentest$.getValue()))
.subscribe({
next: (pentest: Pentest) => {
this.pentest$.next(pentest);
this.initialPentestStatus = pentest.status;
this.notificationService.showPopup('pentest.popup.save.success', PopupType.SUCCESS);
},
error: err => {
console.log(err);
this.notificationService.showPopup('pentest.popup.save.failed', PopupType.FAILURE);
}
});
}
onClickUpdatePentest(): void {
this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus});
this.pentestService.updatePentest(transformPentestToRequestBody(this.pentest$.getValue()))
.subscribe({
next: (pentest: Pentest) => {
this.pentest$.next(pentest);
this.initialPentestStatus = pentest.status;
this.notificationService.showPopup('pentest.popup.update.success', PopupType.SUCCESS);
},
error: err => {
console.log(err);
this.notificationService.showPopup('pentest.popup.update.failed', PopupType.FAILURE);
}
});
}
/**
* @return true if initial pentest Status is different from current pentest status
*/
pentestStatusChanged(): boolean {
if (this.initialPentestStatus !== this.currentStatus) {
this.pentestChanged$.next(true);
} else {
this.pentestChanged$.next(false);
}
return this.pentestChanged$.getValue();
}
/**
* @return the correct nb-status for current pentest-status
*/
getPentestFillStatus(value: PentestStatus): string {
let pentestFillStatus;
switch (value) {
case PentestStatus.NOT_STARTED: {
pentestFillStatus = 'basic';
break;
}
case PentestStatus.OPEN: {
pentestFillStatus = 'info';
break;
}
case PentestStatus.IN_PROGRESS: {
pentestFillStatus = 'warning';
break;
}
case PentestStatus.COMPLETED: {
pentestFillStatus = 'success';
break;
}
default: {
pentestFillStatus = 'basic';
break;
}
}
return pentestFillStatus;
}
} }

View File

@ -19,7 +19,7 @@
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-severity"> <th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-severity">
{{ 'finding.severity' | translate }} {{ 'finding.severity' | translate }}
</th> </th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding" class="cell-severity border-style" fxFill fxLayoutAlign="center center"> <td nbTreeGridCell *nbTreeGridCellDef="let finding" class="cell-severity border-style" fxLayoutAlign="center center">
<app-severity-tag [currentSeverity]="finding.data['severity']"></app-severity-tag> <app-severity-tag [currentSeverity]="finding.data['severity']"></app-severity-tag>
</td> </td>
</ng-container> </ng-container>

View File

@ -7,7 +7,8 @@
.finding-cell { .finding-cell {
// Add style here // Add style here
height: 4.5rem !important; height: 4.5rem !important;
max-height: 4.5rem !important; // max-height: 4.5rem !important;
overflow: hidden;
} }
.finding-cell:hover { .finding-cell:hover {
@ -19,9 +20,15 @@
width: 125px; width: 125px;
max-width: 125px; max-width: 125px;
// border-style: none; // border-style: none;
// ToDo: Fix size issue on lower screen resolution
height: 4.5rem !important; height: 4.5rem !important;
} }
.cell {
height: 4.5rem !important;
max-height: 4.5rem !important;
}
.border-style { .border-style {
border-top-style: none; border-top-style: none;
border-left-style: none; border-left-style: none;

View File

@ -1,7 +1,7 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, mergeMap, tap} from 'rxjs/operators'; import {filter, mergeMap, tap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/notification.service';
@ -18,6 +18,9 @@ import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service'; import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {Store} from '@ngxs/store';
import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
import {ProjectState} from '@shared/stores/project-state/project-state';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -30,16 +33,17 @@ export class PentestFindingsComponent implements OnInit {
constructor(private readonly pentestService: PentestService, constructor(private readonly pentestService: PentestService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>, private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
private notificationService: NotificationService, private notificationService: NotificationService,
private findingDialogService: FindingDialogService) { private findingDialogService: FindingDialogService,
private store: Store) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters); this.dataSource = dataSourceBuilder.create(this.data, this.getters);
} }
@Input()
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null); pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
// HTML only // HTML only
readonly fa = FA; readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;
columns: Array<FindingColumns> = [ columns: Array<FindingColumns> = [
FindingColumns.FINDING_ID, FindingColumns.SEVERITY, FindingColumns.TITLE, FindingColumns.IMPACT, FindingColumns.ACTIONS FindingColumns.FINDING_ID, FindingColumns.SEVERITY, FindingColumns.TITLE, FindingColumns.IMPACT, FindingColumns.ACTIONS
@ -54,23 +58,35 @@ export class PentestFindingsComponent implements OnInit {
expandedGetter: (node: FindingEntry) => !!node.expanded, expandedGetter: (node: FindingEntry) => !!node.expanded,
}; };
// HTML only
notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;
ngOnInit(): void { ngOnInit(): void {
this.loadFindingsData(); this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
this.pentestInfo$.next(selectedPentest);
this.loadFindingsData();
},
error: err => {
console.error(err);
}
});
} }
loadFindingsData(): void { loadFindingsData(): void {
this.pentestService.getFindingsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') this.pentestService.getFindingsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe( .pipe(
untilDestroyed(this), untilDestroyed(this),
filter(isNotNullOrUndefined), /*filter(isNotNullOrUndefined),*/
tap(() => this.loading$.next(true)) tap(() => this.loading$.next(true))
) )
.subscribe({ .subscribe({
next: (findings: Finding[]) => { next: (findings: Finding[]) => {
this.data = transformFindingsToObjectiveEntries(findings); // ToDo: Handle this case before in pipe
if (findings) {
this.data = transformFindingsToObjectiveEntries(findings);
} else {
this.data = [];
}
this.dataSource.setData(this.data, this.getters); this.dataSource.setData(this.data, this.getters);
this.loading$.next(false); this.loading$.next(false);
}, },
@ -104,8 +120,8 @@ export class PentestFindingsComponent implements OnInit {
), ),
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: () => { next: (finding) => {
// ToDo: Parse new Counter to overview / -> dispatch to store maybe already update it this.store.dispatch(new UpdatePentestFindings(finding.id));
this.loadFindingsData(); this.loadFindingsData();
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
}, },

View File

@ -9,14 +9,41 @@ import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module'; import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store'; import {NgxsModule, Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
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: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentIds: []
},
};
describe('PentestInfoComponent', () => { describe('PentestInfoComponent', () => {
let component: PentestInfoComponent; let component: PentestInfoComponent;
let fixture: ComponentFixture<PentestInfoComponent>; let fixture: ComponentFixture<PentestInfoComponent>;
let store: Store;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@ -44,6 +71,11 @@ describe('PentestInfoComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PentestInfoComponent); fixture = TestBed.createComponent(PentestInfoComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance; component = fixture.componentInstance;
component.pentestInfo$.next({ component.pentestInfo$.next({
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112', id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',

View File

@ -3,7 +3,11 @@ import {BehaviorSubject} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import {getPentestInfoForObjective} from '@shared/functions/infos/get-pentest-info-for-objective'; import {getPentestInfoForObjective} from '@shared/functions/infos/get-pentest-info-for-objective';
import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function'; import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Store} from '@ngxs/store';
@UntilDestroy()
@Component({ @Component({
selector: 'app-pentest-info', selector: 'app-pentest-info',
templateUrl: './pentest-info.component.html', templateUrl: './pentest-info.component.html',
@ -11,12 +15,21 @@ import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-ke
}) })
export class PentestInfoComponent implements OnInit { export class PentestInfoComponent implements OnInit {
@Input()
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null); pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
constructor() { } constructor(private store: Store) { }
ngOnInit(): void { ngOnInit(): void {
this.store.selectOnce(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
this.pentestInfo$.next(selectedPentest);
},
error: err => {
console.error(err);
}
});
} }
getPentestHeaderForObjective(refNumber: string): string { getPentestHeaderForObjective(refNumber: string): string {

View File

@ -14,7 +14,41 @@
<h4>{{selectedProjectTitle$.getValue()}} / {{pentest$.getValue().refNumber}}</h4> <h4>{{selectedProjectTitle$.getValue()}} / {{pentest$.getValue().refNumber}}</h4>
<div class="pentest-status-container"> <div class="pentest-status-container">
<app-status-tag [currentStatus]="pentest$.getValue().status"></app-status-tag> <!--ToDo: Add changing dialog here-->
<!--<app-status-tag [currentStatus]="pentest$.getValue().status"></app-status-tag>-->
<!-- Pentest Status Selection -->
<div class="pentest-status-dialog">
<nb-select class="status"
[(selected)]="currentStatus"
shape="round" filled
status="{{getPentestFillStatus(currentStatus)}}">
<nb-option *ngFor="let status of statusTexts" [value]="status.value">
{{ status.translationText | translate }}
</nb-option>
</nb-select>
</div>
<div *ngIf="!pentest$.getValue().id; else updatePentest">
<button nbButton
class="save-pentest-button"
status="primary"
[disabled]="!pentestStatusChanged()"
title="{{ 'global.action.save' | translate }}"
(click)="onClickSavePentest()">
<span class="exit-element-text"> {{ 'global.action.save' | translate }} </span>
</button>
</div>
<ng-template #updatePentest>
<button nbButton
class="save-pentest-button"
status="primary"
[disabled]="!pentestStatusChanged()"
title="{{ 'global.action.update' | translate }}"
(click)="onClickUpdatePentest()">
<span class="exit-element-text"> {{ 'global.action.update' | translate }} </span>
</button>
</ng-template>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@
.exit-button-container { .exit-button-container {
.exit-element-icon { .exit-element-icon {
} }
.exit-element-text { .exit-element-text {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
@ -12,6 +13,35 @@
.pentest-status-container { .pentest-status-container {
display: flex; display: flex;
align-content: flex-end; align-content: flex-end;
margin-right: 0.5rem; // margin-right: 0.5rem;
// height: 5%;
.pentest-status-dialog {
margin: 1rem 2.25rem 1rem 0;
.status {
width: 12rem;
}
.basic {
background-color: nb-theme(color-basic-default);
}
.info {
background-color: nb-theme(color-info-default);
}
.warning {
background-color: nb-theme(color-warning-default);
}
.success {
background-color: nb-theme(color-success-default);
}
}
.save-pentest-button {
margin: 1rem 0 1rem 0;
}
} }
} }

View File

@ -11,8 +11,10 @@ import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state'; import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp', client: 'E Corp',
@ -58,6 +60,9 @@ describe('PentestHeaderComponent', () => {
}), }),
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState]) NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -6,10 +6,13 @@ import {Store} from '@ngxs/store';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {ChangePentest} from '@shared/stores/project-state/project-state.actions'; import {ChangePentest} from '@shared/stores/project-state/project-state.actions';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {Project} from '@shared/models/project.model'; import {Project} from '@shared/models/project.model';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {PentestService} from '@shared/services/pentest.service';
import {StatusText} from '@shared/widgets/status-tag/status-tag.component';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -23,8 +26,27 @@ export class PentestHeaderComponent implements OnInit {
pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null); pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
selectedProjectTitle$: BehaviorSubject<string> = new BehaviorSubject<string>(''); selectedProjectTitle$: BehaviorSubject<string> = new BehaviorSubject<string>('');
pentestChanged$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
// Pentest Status Handler
status = PentestStatus;
currentStatus: PentestStatus = PentestStatus.NOT_STARTED;
private initialPentestStatus: PentestStatus;
// Status Text Translation Texts
readonly statusTexts: Array<StatusText> = [
{value: PentestStatus.NOT_STARTED, translationText: 'pentest.statusText.not_started'},
/* ToDo: Disabled not needed inside pentest */
/*{value: PentestStatus.DISABLED, translationText: 'pentest.statusText.disabled'},*/
{value: PentestStatus.OPEN, translationText: 'pentest.statusText.open'},
{value: PentestStatus.IN_PROGRESS, translationText: 'pentest.statusText.in_progress'},
{value: PentestStatus.COMPLETED, translationText: 'pentest.statusText.completed'}
];
selectedProjectId$: BehaviorSubject<string> = new BehaviorSubject<string>('');
constructor(private store: Store, constructor(private store: Store,
private pentestService: PentestService,
private notificationService: NotificationService,
private readonly router: Router) { private readonly router: Router) {
} }
@ -33,6 +55,7 @@ export class PentestHeaderComponent implements OnInit {
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (selectedProject: Project) => { next: (selectedProject: Project) => {
this.selectedProjectId$.next(selectedProject.id);
this.selectedProjectTitle$.next(selectedProject?.title); this.selectedProjectTitle$.next(selectedProject?.title);
}, },
error: err => { error: err => {
@ -44,6 +67,8 @@ export class PentestHeaderComponent implements OnInit {
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (selectedPentest: Pentest) => { next: (selectedPentest: Pentest) => {
this.currentStatus = selectedPentest.status;
this.initialPentestStatus = selectedPentest.status;
this.pentest$.next(selectedPentest); this.pentest$.next(selectedPentest);
}, },
error: err => { error: err => {
@ -61,4 +86,76 @@ export class PentestHeaderComponent implements OnInit {
} }
).finally(); ).finally();
} }
onClickSavePentest(): void {
this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus});
this.pentestService.savePentest(this.selectedProjectId$.getValue(), transformPentestToRequestBody(this.pentest$.getValue()))
.subscribe({
next: (pentest: Pentest) => {
this.store.dispatch(new ChangePentest(pentest));
this.notificationService.showPopup('pentest.popup.save.success', PopupType.SUCCESS);
},
error: err => {
console.log(err);
this.notificationService.showPopup('pentest.popup.save.failed', PopupType.FAILURE);
}
});
}
onClickUpdatePentest(): void {
this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus});
this.pentestService.updatePentest(transformPentestToRequestBody(this.pentest$.getValue()))
.subscribe({
next: (pentest: Pentest) => {
this.store.dispatch(new ChangePentest(pentest));
this.notificationService.showPopup('pentest.popup.update.success', PopupType.SUCCESS);
},
error: err => {
console.log(err);
this.notificationService.showPopup('pentest.popup.update.failed', PopupType.FAILURE);
}
});
}
/**
* @return true if initial pentest Status is different from current pentest status
*/
pentestStatusChanged(): boolean {
if (this.initialPentestStatus !== this.currentStatus) {
this.pentestChanged$.next(true);
} else {
this.pentestChanged$.next(false);
}
return this.pentestChanged$.getValue();
}
/**
* @return the correct nb-status for current pentest-status
*/
getPentestFillStatus(value: PentestStatus): string {
let pentestFillStatus;
switch (value) {
case PentestStatus.NOT_STARTED: {
pentestFillStatus = 'basic';
break;
}
case PentestStatus.OPEN: {
pentestFillStatus = 'info';
break;
}
case PentestStatus.IN_PROGRESS: {
pentestFillStatus = 'warning';
break;
}
case PentestStatus.COMPLETED: {
pentestFillStatus = 'success';
break;
}
default: {
pentestFillStatus = 'basic';
break;
}
}
return pentestFillStatus;
}
} }

View File

@ -8,8 +8,7 @@ import {Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {catchError, map, switchMap} from 'rxjs/operators'; import {catchError, map, switchMap} from 'rxjs/operators';
import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp-pentests-for-category.function'; import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp-pentests-for-category.function';
import {Finding, FindingDialogBody} from '@shared/models/finding.model'; import {Finding} from '@shared/models/finding.model';
import {Severity} from '@shared/models/severity.enum';
import {Comment} from '@shared/models/comment.model'; import {Comment} from '@shared/models/comment.model';
import {v4 as UUID} from 'uuid'; import {v4 as UUID} from 'uuid';
@ -85,46 +84,43 @@ export class PentestService {
* @param pentestId the id of the project * @param pentestId the id of the project
*/ */
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> { public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
if (pentestId) { return this.http.get<Finding[]>(`${this.apiBaseURL}/${pentestId}/findings`);
return this.http.get<Finding[]>(`${this.apiBaseURL}/${pentestId}/findings`); // return of([]);
} else { /*Todo: Remove mocked Findings?
// return of([]); return of([
// Todo: Remove mocked Findings {
return of([ id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
{ title: 'This is a creative title',
id: 'ca96cc19-88ff-4874-8406-dc892620afd4', description: 'test',
title: 'This is a creative title', impact: 'This impacts only the UI',
description: 'test', severity: Severity.LOW,
impact: 'This impacts only the UI', reproduction: ''
severity: Severity.LOW, },
reproduction: '' {
}, id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
{ title: 'This is a creative title',
id: 'ca96cc19-88ff-4874-8406-dc892620afd4', description: 'test',
title: 'This is a creative title', impact: 'This is impacts some things',
description: 'test', severity: Severity.MEDIUM,
impact: 'This is impacts some things', reproduction: ''
severity: Severity.MEDIUM, },
reproduction: '' {
}, id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
{ title: 'This is a creative title',
id: 'ca96cc19-88ff-4874-8406-dc892620afd4', description: 'test',
title: 'This is a creative title', impact: 'This is impacts a lot',
description: 'test', severity: Severity.HIGH,
impact: 'This is impacts a lot', reproduction: ''
severity: Severity.HIGH, },
reproduction: '' {
}, id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
{ title: 'This is a creative title',
id: 'ca96cc19-88ff-4874-8406-dc892620afd4', description: 'test',
title: 'This is a creative title', impact: 'This is impacts a lot',
description: 'test', severity: Severity.CRITICAL,
impact: 'This is impacts a lot', reproduction: ''
severity: Severity.CRITICAL, }
reproduction: '' ]);*/
}
]);
}
} }
/** /**
@ -141,26 +137,22 @@ export class PentestService {
* @param pentestId the id of the project * @param pentestId the id of the project
*/ */
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> { public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
console.warn('Comments for:', pentestId); return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
if (pentestId) { // return of([]);
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`); /* ToDo: Use mocked Comments?
} else { return of([
// return of([]); {
// Todo: Remove mocked Comments id: 'ca96cc19-88ff-4874-8406-dc892620afd2',
return of([ title: 'This is a creative title',
{ description: 'This is a creative description',
id: 'ca96cc19-88ff-4874-8406-dc892620afd2', relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'],
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',
id: 'ca96cc19-88ff-4874-8406-dc892620afd4', relatedFindings: [],
title: 'This is a creative title', }
description: 'This is a creative description', ]);*/
relatedFindings: [],
}
]);
}
} }
} }

View File

@ -33,3 +33,10 @@ export class ChangePentest {
constructor(public pentest: Pentest) { constructor(public pentest: Pentest) {
} }
} }
export class UpdatePentestFindings {
static readonly type = '[ProjectState] UpdatePentestFindings';
constructor(public findingId: string) {
}
}

View File

@ -1,7 +1,13 @@
import {Action, Selector, State, StateContext} from '@ngxs/store'; import {Action, Selector, State, StateContext} from '@ngxs/store';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Project} from '@shared/models/project.model'; import {Project} from '@shared/models/project.model';
import {ChangeCategory, ChangePentest, ChangeProject, InitProjectState} from '@shared/stores/project-state/project-state.actions'; import {
ChangeCategory,
ChangePentest,
ChangeProject,
InitProjectState,
UpdatePentestFindings
} from '@shared/stores/project-state/project-state.actions';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
@ -80,4 +86,26 @@ export class ProjectState {
selectedPentest: {...pentest, projectId: state.selectedProject.id} selectedPentest: {...pentest, projectId: state.selectedProject.id}
}); });
} }
@Action(UpdatePentestFindings)
updatePentestFindings(ctx: StateContext<ProjectStateModel>, {findingId}: UpdatePentestFindings): void {
const state = ctx.getState();
let stateSelectedPentest: Pentest = state.selectedPentest;
const stateFindingIds: Array<string> = stateSelectedPentest.findingIds || [];
let updatedFindingIds: Array<string> = [];
if (!stateFindingIds.includes(findingId)) {
updatedFindingIds = [...stateFindingIds, findingId];
} else {
// ToDo: Add logic to remove findingId from array
}
// overwrites only findingIds
stateSelectedPentest = {
...stateSelectedPentest,
findingIds: updatedFindingIds
};
// path project state
ctx.patchState({
selectedPentest: stateSelectedPentest
});
}
} }

View File

@ -45,6 +45,7 @@ class ProjectController(private val projectService: ProjectService) {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
fun deleteProject(@PathVariable(value = "id") id: String): Mono<ResponseEntity<ResponseBody>> { fun deleteProject(@PathVariable(value = "id") id: String): Mono<ResponseEntity<ResponseBody>> {
// ToDo: Delete all associated Pentests, Findings and Comments
return this.projectService.deleteProject(id).map{ return this.projectService.deleteProject(id).map{
ResponseEntity.ok().body(it.toProjectDeleteResponseBody()) ResponseEntity.ok().body(it.toProjectDeleteResponseBody())
}.switchIfEmpty { }.switchIfEmpty {