feat: As a user I want to have an export dialog to download my pentest report

This commit is contained in:
Marcel Haag 2023-02-21 15:51:58 +01:00 committed by Cel
parent bb544c71a0
commit 79a2493c37
72 changed files with 1099 additions and 87 deletions

View File

@ -36,6 +36,8 @@
"crypto-js/hmac-sha256", "crypto-js/hmac-sha256",
"crypto-js/lib-typedarrays", "crypto-js/lib-typedarrays",
"js-cookie", "js-cookie",
"chartjs-plugin-annotation",
"chart.js",
"deep-equal", "deep-equal",
"moment-timezone", "moment-timezone",
"uuid" "uuid"

View File

@ -2951,6 +2951,11 @@
} }
} }
}, },
"@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"@nebular/eva-icons": { "@nebular/eva-icons": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/@nebular/eva-icons/-/eva-icons-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@nebular/eva-icons/-/eva-icons-8.0.0.tgz",
@ -5097,6 +5102,14 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "dev": true
}, },
"chart.js": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz",
"integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==",
"requires": {
"@kurkle/color": "^0.3.0"
}
},
"chokidar": { "chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",

View File

@ -34,6 +34,7 @@
"@ngx-translate/http-loader": "^6.0.0", "@ngx-translate/http-loader": "^6.0.0",
"@ngxs/storage-plugin": "^3.7.3", "@ngxs/storage-plugin": "^3.7.3",
"@ngxs/store": "^3.7.3", "@ngxs/store": "^3.7.3",
"chart.js": "^4.2.1",
"eva-icons": "^1.1.3", "eva-icons": "^1.1.3",
"i18n-iso-countries": "^6.8.0", "i18n-iso-countries": "^6.8.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",

View File

@ -24,7 +24,7 @@ import {far} from '@fortawesome/free-regular-svg-icons';
import {NgxsModule} from '@ngxs/store'; import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state'; import {SessionState} from '@shared/stores/session-state/session-state';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {ThemeModule} from '@assets/@theme/theme.module'; import {ThemeModule} from '@assets/@theme/theme.module';
import {HeaderModule} from './header/header.module'; import {HeaderModule} from './header/header.module';
import {HomeModule} from './home/home.module'; import {HomeModule} from './home/home.module';

View File

@ -6,7 +6,7 @@ import {HttpClient, HttpClientModule} from '@angular/common/http';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule, FlexModule} from '@angular/flex-layout'; import {FlexLayoutModule, FlexModule} from '@angular/flex-layout';
import {MomentModule} from 'ngx-moment'; import {MomentModule} from 'ngx-moment';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NbOverlayContainerAdapter, NbSpinnerModule, NbToastrModule} from '@nebular/theme'; import {NbOverlayContainerAdapter, NbSpinnerModule, NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module'; import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';

View File

@ -15,7 +15,7 @@ import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
export class HeaderComponent implements OnInit{ export class HeaderComponent implements OnInit{
readonly fa = FA; readonly fa = FA;
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE; readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
currentTheme = ''; currentTheme = '';
languages = ['en-US', 'de-DE']; languages = ['en-US', 'de-DE'];

View File

@ -21,8 +21,8 @@ import {ReactiveFormsModule} from '@angular/forms';
import {User} from '../../shared/models/user.model'; import {User} from '../../shared/models/user.model';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NotificationService} from '../../shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '../../shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';
const DESIRED_STORE_STATE_SESSION: SessionStateModel = { const DESIRED_STORE_STATE_SESSION: SessionStateModel = {

View File

@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms'; import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {NotificationService, PopupType} from '../../shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {User} from '../../shared/models/user.model'; import {User} from '../../shared/models/user.model';
import {throwError} from 'rxjs'; import {throwError} from 'rxjs';
@ -22,7 +22,6 @@ import {KeycloakService} from 'keycloak-angular';
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
readonly MIN_LENGTH: number = 2; readonly MIN_LENGTH: number = 2;
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE; readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
readonly NOVATEC_NAME = GlobalTitlesVariables.NOVATEC_NAME;
// ToDo: Remove after adding real authentication // ToDo: Remove after adding real authentication
private readonly user = new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US'); private readonly user = new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US');

View File

@ -4,7 +4,7 @@ import {LoginComponent} from './login.component';
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 {NotificationService} from '../../shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {LoginRoutingModule} from './login-routing.module'; import {LoginRoutingModule} from './login-routing.module';
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme'; import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme';
import {ReactiveFormsModule} from '@angular/forms'; import {ReactiveFormsModule} from '@angular/forms';

View File

@ -14,8 +14,9 @@
<div class="button-container"> <div class="button-container">
<nb-actions size="medium"> <nb-actions size="medium">
<nb-action> <nb-action>
<!--status="button-outline-basic-text-color"-->
<button nbButton <button nbButton
status="button-outline-basic-text-color" status="primary"
shape="round" shape="round"
(click)="onClickEditPentestProject()"> (click)="onClickEditPentestProject()">
<fa-icon [icon]="fa.faEdit" <fa-icon [icon]="fa.faEdit"
@ -27,7 +28,7 @@
<button nbButton hero <button nbButton hero
status="info" status="info"
shape="round" shape="round"
(click)="onClickExportPentest()"> (click)="onClickExportPentestReport()">
<fa-icon [icon]="fa.faFileExport" <fa-icon [icon]="fa.faFileExport"
class="element-icon fa-lg"></fa-icon> class="element-icon fa-lg"></fa-icon>
<span class="element-text">{{ 'global.action.export' | translate }}</span> <span class="element-text">{{ 'global.action.export' | translate }}</span>

View File

@ -12,16 +12,18 @@ 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 {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbActionsModule, NbIconModule} from '@nebular/theme'; import {NbActionsModule, NbIconModule} from '@nebular/theme';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
import {ProjectServiceMock} from '@shared/services/project.service.mock'; import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service'; import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock'; import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
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 {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {ExportReportDialogServiceMock} from '@shared/modules/export-report-dialog/service/export-report-dialog.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {
@ -78,6 +80,7 @@ describe('ObjectiveHeaderComponent', () => {
providers: [ providers: [
{provide: ProjectService, useValue: new ProjectServiceMock()}, {provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock}, {provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: ExportReportDialogService, useClass: ExportReportDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock}, {provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()} {provide: NotificationService, useValue: new NotificationServiceMock()}
] ]

View File

@ -9,11 +9,13 @@ import {BehaviorSubject} from 'rxjs';
import {Project, ProjectDialogBody} from '@shared/models/project.model'; import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
import {filter, mergeMap} from 'rxjs/operators'; import {filter, mergeMap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service'; import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {InitProjectState} from '@shared/stores/project-state/project-state.actions'; import {InitProjectState} from '@shared/stores/project-state/project-state.actions';
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {ExportReportDialogComponent} from '@shared/modules/export-report-dialog/export-report-dialog.component';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -28,9 +30,10 @@ export class ObjectiveHeaderComponent implements OnInit {
constructor(private store: Store, constructor(private store: Store,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private projectService: ProjectService,
private dialogService: DialogService, private dialogService: DialogService,
private projectDialogService: ProjectDialogService, private projectDialogService: ProjectDialogService,
private projectService: ProjectService,
private exportReportDialogService: ExportReportDialogService,
private readonly router: Router) { private readonly router: Router) {
} }
@ -87,8 +90,30 @@ export class ObjectiveHeaderComponent implements OnInit {
}); });
} }
onClickExportPentest(): void { onClickExportPentestReport(): void {
// tslint:disable-next-line:no-console this.exportReportDialogService.openExportReportDialog(
console.info('To be implemented..'); ExportReportDialogComponent,
this.selectedProject$.getValue(),
{
closeOnEsc: true,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: true
}
).pipe(
filter(value => !!value),
/*ToDo: Needed?*/
/*mergeMap((value: ProjectDialogBody) => this.projectService.updateProject(this.selectedProject$.getValue().id, value)),*/
untilDestroyed(this)
).subscribe({
next: () => {
// ToDo: Open report in new Tab or just download it?
// this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
// this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE);
}
});
} }
} }

View File

@ -22,6 +22,7 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
import {CommonAppModule} from '../common-app.module'; import {CommonAppModule} from '../common-app.module';
import {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module'; import {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module';
import {ExportReportDialogModule} from '@shared/modules/export-report-dialog/export-report-dialog.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -50,6 +51,7 @@ import {ObjectiveOverviewRoutingModule} from './objective-overview-routing.modul
FontAwesomeModule, FontAwesomeModule,
FlexLayoutModule, FlexLayoutModule,
NbActionsModule, NbActionsModule,
ExportReportDialogModule,
ObjectiveOverviewRoutingModule ObjectiveOverviewRoutingModule
], ],
exports: [ exports: [

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model'; import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/api/pentest.service';
import {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';

View File

@ -14,8 +14,8 @@ 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 {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {MockComponent} from 'ng-mocks'; import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';

View File

@ -3,7 +3,7 @@ import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import { import {
@ -19,11 +19,11 @@ import {Store} from '@ngxs/store';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service'; import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentService} from '@shared/services/comment.service'; import {CommentService} from '@shared/services/api/comment.service';
import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions'; import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component'; import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Finding} from '@shared/models/finding.model'; import {Finding} from '@shared/models/finding.model';
import {FindingService} from '@shared/services/finding.service'; import {FindingService} from '@shared/services/api/finding.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({

View File

@ -11,8 +11,8 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
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 {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {

View File

@ -5,8 +5,8 @@ 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} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/api/pentest.service';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({

View File

@ -9,8 +9,8 @@ import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store'; 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 {NbButtonModule, NbTreeGridModule} from '@nebular/theme'; import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {MockComponent} from 'ng-mocks'; import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';

View File

@ -1,10 +1,10 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/api/pentest.service';
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 {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import { import {
Finding, Finding,
FindingDialogBody, FindingDialogBody,
@ -22,7 +22,7 @@ import {Store} from '@ngxs/store';
import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions'; import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
import {ProjectState} from '@shared/stores/project-state/project-state'; import {ProjectState} from '@shared/stores/project-state/project-state';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {FindingService} from '@shared/services/finding.service'; import {FindingService} from '@shared/services/api/finding.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({

View File

@ -11,8 +11,8 @@ 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 {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {

View File

@ -9,9 +9,9 @@ import {BehaviorSubject} from 'rxjs';
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, transformPentestToRequestBody} from '@shared/models/pentest.model'; import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/api/pentest.service';
import {StatusText} from '@shared/widgets/status-tag/status-tag.component'; import {StatusText} from '@shared/widgets/status-tag/status-tag.component';
@UntilDestroy() @UntilDestroy()

View File

@ -8,7 +8,7 @@ import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} from
import {FlexLayoutModule} from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
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 {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
@ -16,9 +16,9 @@ import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state'; import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ProjectServiceMock} from '@shared/services/project.service.mock'; import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ThemeModule} from '@assets/@theme/theme.module'; import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';

View File

@ -3,8 +3,8 @@ import * as FA from '@fortawesome/free-solid-svg-icons';
import {Project, ProjectDialogBody} from '@shared/models/project.model'; import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
@ -17,7 +17,7 @@ import {ProjectDialogService} from '@shared/modules/project-dialog/service/proje
styleUrls: ['./project-overview.component.scss'] styleUrls: ['./project-overview.component.scss']
}) })
export class ProjectOverviewComponent implements OnInit { export class ProjectOverviewComponent implements OnInit {
// HTML only
readonly fa = FA; readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);

View File

@ -10,20 +10,23 @@ import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store'; import {NgxsModule, Store} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state'; import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NbCardModule, NbLayoutModule} from '@nebular/theme'; import {NbCardModule, NbDialogRef, NbLayoutModule} from '@nebular/theme';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';
import {ObjectiveOverviewModule} from '../../objective-overview'; import {ObjectiveOverviewModule} from '../../objective-overview';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
import {ProjectServiceMock} from '@shared/services/project.service.mock'; import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service'; import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock'; import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
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 {createSpyObj} from '@shared/modules/finding-dialog/finding-dialog.component.spec';
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {ExportReportDialogServiceMock} from '@shared/modules/export-report-dialog/service/export-report-dialog.service.mock';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {
@ -58,6 +61,8 @@ describe('ProjectComponent', () => {
let store: Store; let store: Store;
beforeEach(async () => { beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ declarations: [
ProjectComponent ProjectComponent
@ -83,8 +88,10 @@ describe('ProjectComponent', () => {
providers: [ providers: [
KeycloakService, KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()}, {provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock}, {provide: DialogService, useClass: DialogServiceMock},
{provide: NbDialogRef, useValue: dialogSpy},
{provide: ExportReportDialogService, useClass: ExportReportDialogServiceMock},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: NotificationService, useClass: NotificationServiceMock} {provide: NotificationService, useClass: NotificationServiceMock}
] ]
}) })

View File

@ -11,6 +11,8 @@
"action.update": "Speichern", "action.update": "Speichern",
"action.edit": "Editieren", "action.edit": "Editieren",
"action.export": "Exportieren", "action.export": "Exportieren",
"action.download": "Herunterladen",
"action.report": "Bericht",
"action.reset": "Zurücksetzen", "action.reset": "Zurücksetzen",
"action.yes": "Ja", "action.yes": "Ja",
"action.no": "Nein", "action.no": "Nein",
@ -19,7 +21,8 @@
"no.progress": "Kein Fortschritt", "no.progress": "Kein Fortschritt",
"validationMessage": { "validationMessage": {
"inputNotMatching": "Eingabe stimmt nicht überein!" "inputNotMatching": "Eingabe stimmt nicht überein!"
} },
"project": "Projekt"
}, },
"languageKeys":{ "languageKeys":{
"de-DE": "Deutsch", "de-DE": "Deutsch",
@ -43,6 +46,19 @@
"failed": "Benutzername oder Passwort falsch", "failed": "Benutzername oder Passwort falsch",
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen" "unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
}, },
"report": {
"dialog": {
"header": "Penetrationstestbereicht exportieren",
"formatLabel": "Wählen Sie das Exportformat für den Bericht:",
"languageLabel": "Wählen Sie die Berichtssprache:"
},
"popup": {
"generation.success": "Bericht erfolgreich generiert",
"generation.failed": "Bericht konnte nicht generiert werden"
},
"generate": "Bericht generieren",
"hint": "{{completedObjectivesNumber}} Ihrer abgeschlossenen Ziele wird in den Pentestbericht aufgenommen."
},
"project": { "project": {
"title.label": "Projekt Titel", "title.label": "Projekt Titel",
"client.label": "Name des Auftraggebers", "client.label": "Name des Auftraggebers",

View File

@ -11,6 +11,8 @@
"action.update": "Update", "action.update": "Update",
"action.edit": "Edit", "action.edit": "Edit",
"action.export": "Export", "action.export": "Export",
"action.download": "Download",
"action.report": "Report",
"action.reset": "Reset", "action.reset": "Reset",
"action.yes": "Yes", "action.yes": "Yes",
"action.no": "No", "action.no": "No",
@ -19,7 +21,8 @@
"no.progress": "No progress", "no.progress": "No progress",
"validationMessage": { "validationMessage": {
"inputNotMatching": "Input does not match!" "inputNotMatching": "Input does not match!"
} },
"project": "Project"
}, },
"languageKeys":{ "languageKeys":{
"de-DE": "German", "de-DE": "German",
@ -43,6 +46,19 @@
"failed": "Wrong username or password", "failed": "Wrong username or password",
"unauthorized": "User not found. Please register and try again" "unauthorized": "User not found. Please register and try again"
}, },
"report": {
"dialog": {
"header": "Export Penetrationtest Report",
"formatLabel": "Pick export format for report:",
"languageLabel": "Pick report language:"
},
"popup": {
"generation.success": "Report generation successful",
"generation.failed": "Report could not be generated"
},
"generate": "Generate Report",
"hint": "{{completedObjectivesNumber}} of your completed objective(s) will be included in the pentest report."
},
"project": { "project": {
"title.label": "Project Title", "title.label": "Project Title",
"client.label": "Name of Client", "client.label": "Name of Client",

View File

@ -8,4 +8,5 @@ export const environment = {
// backend service // backend service
apiEndpoint: 'http://localhost:8443', apiEndpoint: 'http://localhost:8443',
reportEndpoint: 'http://localhost:8444'
}; };

View File

@ -13,6 +13,7 @@ export const environment = {
// backend service // backend service
apiEndpoint: 'http://localhost:8443', apiEndpoint: 'http://localhost:8443',
reportEndpoint: 'http://localhost:8444'
}; };
/* /*

View File

@ -1,4 +1,4 @@
export const GlobalTitlesVariables = { export const GlobalTitlesVariables = {
SECURITYC4PO_TITLE: 'Security C4PO', SECURITYC4PO_TITLE: 'Security C4PO',
NOVATEC_NAME: 'Novatec' TOTAL_OWASP_OBJECTIVES: 95
}; };

View File

@ -0,0 +1,13 @@
/**
* Method is used to download file.
* @param data - Array Buffer data
* @param type - type of the document.
*/
export function downloadFile(data: any, type: string): void {
const blob = new Blob([data], {type});
const url = window.URL.createObjectURL(blob);
const pwa = window.open(url);
if (!pwa || pwa.closed || typeof pwa.closed === 'undefined') {
alert('Please disable your Pop-up blocker and try again.');
}
}

View File

@ -17,6 +17,7 @@ export class TokenInterceptor implements HttpInterceptor {
private static listOfKeycloakRelevantHosts(): { origin: string }[] { private static listOfKeycloakRelevantHosts(): { origin: string }[] {
const relevantList = new Array<{ origin: string }>(); const relevantList = new Array<{ origin: string }>();
relevantList.push({origin: getOriginByUrl(environment.apiEndpoint)}); relevantList.push({origin: getOriginByUrl(environment.apiEndpoint)});
relevantList.push({origin: getOriginByUrl(environment.reportEndpoint)});
relevantList.push({origin: getOriginByUrl(environment.keycloakURL)}); relevantList.push({origin: getOriginByUrl(environment.keycloakURL)});
return relevantList; return relevantList;

View File

@ -1,3 +1,5 @@
import {PentestStatus} from '@shared/models/pentest-status.model';
export class Project { export class Project {
id: string; id: string;
client: string; client: string;
@ -5,7 +7,8 @@ export class Project {
createdAt: Date; createdAt: Date;
tester: string; tester: string;
summary: string; summary: string;
testingProgress: number; projectPentests?: Array<ProjectPentests>;
testingProgress?: number;
createdBy: string; createdBy: string;
constructor(id: string, constructor(id: string,
@ -13,7 +16,8 @@ export class Project {
title: string, title: string,
createdAt: Date, createdAt: Date,
tester: string, tester: string,
testingProgress: number, projectPentests?: Array<ProjectPentests>,
testingProgress?: number,
summary?: string, summary?: string,
createdBy?: string) { createdBy?: string) {
this.id = id; this.id = id;
@ -21,6 +25,7 @@ export class Project {
this.title = title; this.title = title;
this.createdAt = createdAt; this.createdAt = createdAt;
this.tester = tester; this.tester = tester;
this.projectPentests = projectPentests;
this.testingProgress = testingProgress; this.testingProgress = testingProgress;
this.summary = summary; this.summary = summary;
this.createdBy = createdBy; this.createdBy = createdBy;
@ -31,5 +36,10 @@ export interface ProjectDialogBody {
title: string; title: string;
client: string; client: string;
tester: string; tester: string;
// ToDo: summary: string; summary: string;
}
export class ProjectPentests {
pentestId: string;
status: PentestStatus;
} }

View File

@ -24,8 +24,8 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module'; import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http'; import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import Mock = jest.Mock; import Mock = jest.Mock;

View File

@ -0,0 +1,67 @@
<nb-card #dialog accent="{{dialogData?.options[0].accentColor}}" class="export-report-dialog">
<nb-card-header fxLayoutAlign="start center" class="export-report-header">
{{ dialogData?.options[0].headerLabelKey | translate }}
</nb-card-header>
<nb-card-body>
<div fxLayout="column" fxLayoutGap="1rem" fxLayoutAlign="start start">
<label class="export-format-label">
{{ 'global.project' | translate }}:
</label>
<b class="project-title">
{{selectedProject$.getValue()?.title}}
</b>
<!--Chart Objective Component-->
<div fxLayout="column" fxLayoutGap="2rem" fxLayoutAlign="center center">
<app-objective-chart [projectPentestData]="selectedProject$.getValue().projectPentests"></app-objective-chart>
<span
class="hint"> {{'popup.info' | translate}} {{ 'report.hint' | translate: this.completedProjectPentests$?.getValue() }}
</span>
</div>
<!--Export Language Radio Selection-->
<label class="export-language-label">
{{ 'report.dialog.languageLabel' | translate }}
</label>
<nb-radio-group name="language" [formControl]="exportReportLanguageControl"
class="export-radio-buttons languageContainer" status="info">
<nb-radio value="{{exportLanguages.ENGLISH}}">
<img src="../../assets/images/flags/{{exportLanguages.ENGLISH}}.svg" class="flag" width="25rem" height="16rem"
alt="">
</nb-radio>
<nb-radio disabled value="{{exportLanguages.GERMAN}}">
<img src="../../assets/images/flags/{{exportLanguages.GERMAN}}.svg" class="flag" width="25rem" height="16rem"
alt="">
</nb-radio>
</nb-radio-group>
<!--Export Format Radio Selection-->
<label class="export-format-label">
{{ 'report.dialog.formatLabel' | translate }}
</label>
<nb-radio-group name="format" [formControl]="exportReportFormatControl" class="export-radio-buttons"
status="info">
<nb-radio value="{{exportFormats.PDF}}">
{{exportFormats.PDF}}
</nb-radio>
<nb-radio disabled value="{{exportFormats.CSV}}">
{{exportFormats.CSV}}
</nb-radio>
<nb-radio disabled value="{{exportFormats.HTML}}">
{{exportFormats.HTML}}
</nb-radio>
</nb-radio-group>
</div>
</nb-card-body>
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
<button nbButton status="info" size="small" class="dialog-button generate-report-button"
[disabled]="(loading$.getValue() === true || completedProjectPentests$?.getValue().completedObjectivesNumber < 1)"
(click)="onClickExport(exportReportFormatControl.value, exportReportLanguageControl.value)">
<fa-icon [icon]="fa.faFileExport"
class="element-icon fa-lg"></fa-icon>
<span class="element-text"> {{ dialogData?.options[0].buttonKey | translate}} </span>
</button>
<button nbButton status="danger" size="small" class="dialog-button" (click)="onClickClose()">
{{ 'global.action.cancel' | translate }}
</button>
</nb-card-footer>
</nb-card>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,84 @@
@import "../../../assets/@theme/styles/_dialog.scss";
@import '../../../assets/@theme/styles/themes';
.export-report-dialog {
width: 45.25rem !important;
height: 54.25rem;
.export-report-header {
height: 8vh;
font-size: 1.5rem;
}
.languageContainer {
display: flex;
max-width: 8rem;
min-width: 8rem;
.flag {
// object-fit: contain;
margin-top: 0.5rem;
}
}
.hint {
padding-bottom: 0.5rem;
color: nb-theme(color-danger-default);
}
.export-radio-buttons {
float: left;
clear: none;
margin-left: 1rem;
}
nb-form-field {
padding: 0.5rem 0 0.75rem;
}
.label {
display: block;
font-size: 0.95rem;
padding-bottom: 0.5rem;
}
.project-title {
font-size: 1.5rem;
padding: 0.25rem 0.5rem 0.5rem;
}
.form-field {
width: 26.75rem;
// width: 30rem !important;
margin-bottom: 0.5rem;
}
.form-textarea {
width: 26.75rem !important;
// width: 30rem !important;
height: 8rem;
}
.form-textarea:disabled {
width: 26.75rem !important;
// width: 30rem !important;
background-color: nb-theme(color-basic-transparent-focus);
height: 8rem;
}
.generate-report-button {
width: calc(45.25rem - 5.75rem) !important;
}
.element-text {
font-size: 1rem !important;
text-transform: uppercase !important;
padding-left: 0.5rem;
}
.error-text {
float: left;
color: nb-theme(color-danger-default);
}
}

View File

@ -0,0 +1,123 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ExportReportDialogComponent} from './export-report-dialog.component';
import {CommonModule} from '@angular/common';
import {
NB_DIALOG_CONFIG,
NbButtonModule,
NbCardModule,
NbDialogRef,
NbFormFieldModule,
NbInputModule,
NbLayoutModule, NbRadioModule,
NbTagModule
} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {NG_VALUE_ACCESSOR, ReactiveFormsModule} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {createSpyObj} from '@shared/modules/finding-dialog/finding-dialog.component.spec';
import {Project, ProjectPentests} from '@shared/models/project.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {ObjectiveChartModule} from '@shared/modules/objective-chart/objective-chart.module';
import {forwardRef} from '@angular/core';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
describe('ExportReportDialogComponent', () => {
let component: ExportReportDialogComponent;
let fixture: ComponentFixture<ExportReportDialogComponent>;
beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({
declarations: [
ExportReportDialogComponent
],
imports: [
NbCardModule,
NbButtonModule,
NbFormFieldModule,
NbInputModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
ReactiveFormsModule,
NbRadioModule,
ObjectiveChartModule,
ReactiveFormsModule,
BrowserAnimationsModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NbDialogRef, useValue: dialogSpy},
{provide: NB_DIALOG_CONFIG, useValue: mockedExportPentestDialogData}
]
})
.compileComponents();
});
beforeEach(() => {
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedExportPentestDialogData});
fixture = TestBed.createComponent(ExportReportDialogComponent);
component = fixture.componentInstance;
component.selectedProject$.next(mockProject);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
const mockedPentests: ProjectPentests[] = [
{
pentestId: '122',
status: PentestStatus.COMPLETED
}
];
const mockProject: Project = {
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',
summary: '',
projectPentests: mockedPentests,
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
};
export const mockedExportPentestDialogData = {
form: {},
options: [
{
headerLabelKey: 'finding.create.header',
buttonKey: 'global.action.save',
accentColor: 'info',
additionalData: mockProject
},
]
};

View File

@ -0,0 +1,136 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FormControl} from '@angular/forms';
import {GenericDialogData} from '@shared/models/generic-dialog-data';
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
import {ReportingService} from '@shared/services/reporting/reporting.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Project} from '@shared/models/project.model';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {BehaviorSubject, Observable} from 'rxjs';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {ProjectService} from '@shared/services/api/project.service';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {tap} from 'rxjs/operators';
import {downloadFile} from '@shared/functions/download-file.function';
@Component({
selector: 'app-export-report-dialog',
templateUrl: './export-report-dialog.component.html',
styleUrls: ['./export-report-dialog.component.scss']
})
@UntilDestroy()
export class ExportReportDialogComponent implements OnInit {
constructor(
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
private projectService: ProjectService,
private reportingService: ReportingService,
private readonly notificationService: NotificationService,
protected dialogRef: NbDialogRef<ExportReportDialogComponent>
) {
}
// HTML
readonly fa = FA;
// form control elements
exportReportFormatControl = new FormControl(ExportFormatOptions.PDF);
exportReportLanguageControl = new FormControl(ExportLanguageOptions.ENGLISH);
// exports
exportFormats = ExportFormatOptions;
exportLanguages = ExportLanguageOptions;
dialogData: GenericDialogData;
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
completedProjectPentests$: BehaviorSubject<any> = new BehaviorSubject<any>({completedObjectivesNumber: 0});
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
ngOnInit(): void {
this.dialogData = this.data;
this.loadEvaluatedProject();
}
loadEvaluatedProject(): void {
// Get project id from dialog data
const projectId = this.dialogData.options[0].additionalData.id;
// Request project information by id
this.projectService.getEvaluatedProjectById(projectId)
.pipe(
tap(() => this.loading$.next(true)),
untilDestroyed(this)
)
.subscribe({
next: (project: Project) => {
this.selectedProject$.next(project);
const completedPentestObjectives = project.projectPentests.filter(pentest => pentest.status === PentestStatus.COMPLETED);
this.completedProjectPentests$.next({completedObjectivesNumber: completedPentestObjectives.length});
this.loading$.next(false);
},
error: err => {
console.log(err);
this.notificationService.showPopup('project.popup.not.found', PopupType.FAILURE);
this.loading$.next(false);
}
});
}
onClickExport(reportFormat: string, reportLanguage: string): void {
// Loading is true as long as there is a response from the reporting service
this.loading$.next(true);
// Export pentest in choosen format
switch (reportFormat) {
case ExportFormatOptions.PDF: {
this.reportingService.getReportPDFforProjectById(this.selectedProject$.getValue().id).pipe(
untilDestroyed(this)
).subscribe({
next: (response) => {
downloadFile(response, 'application/pdf');
this.loading$.next(false);
this.notificationService.showPopup('report.popup.generation.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
this.loading$.next(false);
this.notificationService.showPopup('report.popup.generation.failed', PopupType.FAILURE);
}
});
break;
}
case ExportFormatOptions.CSV: {
this.loading$.next(false);
this.notificationService.showPopup('report.popup.generation.failed', PopupType.FAILURE);
break;
}
case ExportFormatOptions.HTML: {
this.loading$.next(false);
this.notificationService.showPopup('report.popup.generation.failed', PopupType.FAILURE);
break;
}
default: {
this.loading$.next(false);
this.notificationService.showPopup('report.popup.generation.failed', PopupType.FAILURE);
break;
}
}
}
onClickClose(): void {
this.dialogRef.close();
}
// HTML only
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
}
export enum ExportFormatOptions {
PDF = 'PDF',
CSV = 'CSV',
HTML = 'HTML'
}
export enum ExportLanguageOptions {
ENGLISH = 'en-US',
GERMAN = 'de-DE'
}

View File

@ -0,0 +1,40 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {CommonAppModule} from '../../../app/common-app.module';
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbRadioModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core';
import {ReactiveFormsModule} from '@angular/forms';
import {ExportReportDialogComponent} from '@shared/modules/export-report-dialog/export-report-dialog.component';
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {ReportingService} from '@shared/services/reporting/reporting.service';
import {ObjectiveChartModule} from '@shared/modules/objective-chart/objective-chart.module';
@NgModule({
declarations: [
ExportReportDialogComponent
],
imports: [
CommonModule,
CommonAppModule,
NbCardModule,
NbButtonModule,
NbFormFieldModule,
NbInputModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
ReactiveFormsModule,
NbRadioModule,
ObjectiveChartModule
],
providers: [
ExportReportDialogService,
ReportingService
],
entryComponents: [
ExportReportDialogComponent
]
})
export class ExportReportDialogModule { }

View File

@ -0,0 +1,17 @@
import {ComponentType} from '@angular/cdk/overlay';
import {NbDialogConfig} from '@nebular/theme';
import {Observable, of} from 'rxjs';
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {Project} from '@shared/models/project.model';
export class ExportReportDialogServiceMock implements Required<ExportReportDialogService> {
dialog: any;
openExportReportDialog(
componentOrTemplateRef: ComponentType<any>,
project: Project | undefined,
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
return of(undefined);
}
}

View File

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

View File

@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import {NbDialogConfig, NbDialogService} from '@nebular/theme';
import {GenericDialogData} from '@shared/models/generic-dialog-data';
import {ComponentType} from '@angular/cdk/overlay';
import {Observable} from 'rxjs';
import {ExportReportDialogComponent} from '@shared/modules/export-report-dialog/export-report-dialog.component';
import {Project} from '@shared/models/project.model';
@Injectable()
export class ExportReportDialogService {
constructor(private readonly dialog: NbDialogService) {
}
private readonly MIN_LENGTH: number = 4;
static addDataToDialogConfig(
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
reportData?: GenericDialogData
): Partial<NbDialogConfig<Partial<any> | string>> {
return {
context: {data: reportData},
closeOnEsc: dialogOptions?.closeOnEsc || false,
hasScroll: dialogOptions?.hasScroll || false,
autoFocus: dialogOptions?.autoFocus || false,
closeOnBackdropClick: dialogOptions?.closeOnBackdropClick || false
};
}
public openExportReportDialog(componentOrTemplateRef: ComponentType<any>,
project?: Project,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
let dialogData: GenericDialogData;
// Setup ExportReportDialogBody
dialogData = {
form: {
},
options: [
{
headerLabelKey: 'report.dialog.header',
buttonKey: 'report.generate',
accentColor: 'info',
additionalData: project
},
]
};
// Merge dialog config with finding data
dialogOptions = ExportReportDialogService.addDataToDialogConfig(config, dialogData);
return this.dialog.open(ExportReportDialogComponent, dialogOptions).onClose;
}
}

View File

@ -19,8 +19,8 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module'; import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http'; import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {Severity} from '@shared/models/severity.enum'; import {Severity} from '@shared/models/severity.enum';

View File

@ -0,0 +1,3 @@
<div class="objective-chart" fxLayout="column" fxLayoutGap="1rem" fxLayoutAlign="start start">
<canvas id="PentestObjectiveChart" class="chart" >{{ chart }}</canvas>
</div>

View File

@ -0,0 +1,9 @@
.objective-chart {
width: 40rem !important;
height: 16rem;
.chart {
// height: 100%;
// width: 100%;
}
}

View File

@ -0,0 +1,60 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ObjectiveChartComponent} from './objective-chart.component';
import {CommonModule} from '@angular/common';
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbLayoutModule, NbTagModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {ReactiveFormsModule} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('ObjectiveChartComponent', () => {
let component: ObjectiveChartComponent;
let fixture: ComponentFixture<ObjectiveChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ObjectiveChartComponent
],
imports: [
CommonModule,
NbLayoutModule,
NbCardModule,
FlexLayoutModule,
NbInputModule,
NbTagModule,
ReactiveFormsModule,
BrowserAnimationsModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState]),
HttpClientModule,
HttpClientTestingModule
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,128 @@
import {Component, Input, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {GlobalTitlesVariables} from '@shared/config/global-variables';
import {ProjectPentests} from '@shared/models/project.model';
import Chart from 'chart.js/auto';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {TranslateService} from '@ngx-translate/core';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
@Component({
selector: 'app-objective-chart',
templateUrl: './objective-chart.component.html',
styleUrls: ['./objective-chart.component.scss']
})
@UntilDestroy()
export class ObjectiveChartComponent implements OnInit {
readonly fa = FA;
readonly TOTAL_OWASP_OBJECTIVES: number = GlobalTitlesVariables.TOTAL_OWASP_OBJECTIVES;
@Input() projectPentestData: ProjectPentests[] = [];
chart: any;
data: any;
options: any;
pentestStatusColors = {
disabledDefault: '#000',
notStartedDefault: '#8f9bb3',
info: '#34a4fe',
warning: '#ffab00',
success: '#01d68f'
};
constructor(private translateService: TranslateService) {
}
// HTML only
status = PentestStatus;
readonly pentestStatusLabels: Array<string> = [
'pentest.statusText.disabled',
'pentest.statusText.not_started',
'pentest.statusText.open',
'pentest.statusText.in_progress',
'pentest.statusText.completed'
];
translatedPentestStatusLabels: Array<string> = [];
ngOnInit(): void {
this.translateLabels();
this.createChart();
}
createChart(): void {
// Sort objectives by status
const disabledPentests: ProjectPentests[]
= this.projectPentestData.filter(projectPentest => projectPentest.status === PentestStatus.DISABLED);
const openPentests: ProjectPentests[]
= this.projectPentestData.filter(projectPentest => projectPentest.status === PentestStatus.OPEN);
const inProgressPentests: ProjectPentests[]
= this.projectPentestData.filter(projectPentest => projectPentest.status === PentestStatus.IN_PROGRESS);
const completedPentests: ProjectPentests[]
= this.projectPentestData.filter(projectPentest => projectPentest.status === PentestStatus.COMPLETED);
// Find not started pentest by removing other pentests from total objective count
const notStartedPentests: number
= this.TOTAL_OWASP_OBJECTIVES - this.projectPentestData.length;
// Setup data for chart
const pentestData = [
disabledPentests.length,
notStartedPentests,
openPentests.length,
inProgressPentests.length,
completedPentests.length
];
// increase-legend-spacing
const increseLegenStylePlugin = {
id: 'increase-legend-spacing',
beforeInit(chart: any): void {
// Get reference to the original fit function
const originalFit = chart.legend.fit;
// Override the fit function
chart.legend.fit = function fit(): void {
// Call original function and bind scope in order to use `this` correctly inside it
originalFit.bind(chart.legend)();
// Change the height as suggested in another answers
this.height += 25;
};
}
};
// Build Chart
this.chart = new Chart('PentestObjectiveChart', {
type: 'pie', // this denotes tha type of chart
data: {// values on X-Axis
labels: this.translatedPentestStatusLabels.length ? this.translatedPentestStatusLabels : this.pentestStatusLabels,
datasets: [{
label: ' ',
data: pentestData,
backgroundColor: [
this.pentestStatusColors.disabledDefault,
this.pentestStatusColors.notStartedDefault,
this.pentestStatusColors.info,
this.pentestStatusColors.warning,
this.pentestStatusColors.success,
],
hoverOffset: 1
}],
},
options: {
aspectRatio: 2.5,
},
plugins: [increseLegenStylePlugin]
});
}
private translateLabels(): void {
this.pentestStatusLabels.forEach((label, index) => {
this.translateService.get(label)
.pipe(untilDestroyed(this))
.subscribe((translated: string): void => {
this.translatedPentestStatusLabels[index] = translated;
});
});
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {ObjectiveChartComponent} from '@shared/modules/objective-chart/objective-chart.component';
import {FlexLayoutModule} from '@angular/flex-layout';
@NgModule({
declarations: [
ObjectiveChartComponent
],
imports: [
CommonModule,
FlexLayoutModule
],
exports: [
ObjectiveChartComponent
]
})
export class ObjectiveChartModule { }

View File

@ -2,7 +2,7 @@
@import '../../../assets/@theme/styles/themes'; @import '../../../assets/@theme/styles/themes';
.project-dialog { .project-dialog {
width: 40rem !important; width: 34rem !important;
height: 42.5rem; height: 42.5rem;
.project-dialog-header { .project-dialog-header {

View File

@ -17,8 +17,8 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../app/common-app.module'; import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http'; import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NotificationService} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ReactiveFormsModule, Validators} from '@angular/forms'; import {ReactiveFormsModule, Validators} from '@angular/forms';

View File

@ -3,7 +3,7 @@ import {User} from '@shared/models/user.model';
import {NumberAndDateFormatSystem} from '@shared/models/number-and-date-time-format.model'; import {NumberAndDateFormatSystem} from '@shared/models/number-and-date-time-format.model';
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state'; import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
import {NgxsModule, Store} from '@ngxs/store'; import {NgxsModule, Store} from '@ngxs/store';
import {UserService} from '@shared/services/user.service'; import {UserService} from '@shared/services/user-service/user.service';
import {inject, TestBed} from '@angular/core/testing'; import {inject, TestBed} from '@angular/core/testing';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {HttpLoaderFactory} from '../../app/common-app.module'; import {HttpLoaderFactory} from '../../app/common-app.module';

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {environment} from '../../environments/environment'; import {environment} from '../../../environments/environment';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {environment} from '../../environments/environment'; import {environment} from '../../../environments/environment';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';

View File

@ -1,5 +1,5 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment'; import {environment} from '../../../environments/environment';
import {HttpClient, HttpParams} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';

View File

@ -1,4 +1,4 @@
import {ProjectService} from '@shared/services/project.service'; import {ProjectService} from '@shared/services/api/project.service';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {Project, ProjectDialogBody} from '@shared/models/project.model'; import {Project, ProjectDialogBody} from '@shared/models/project.model';
@ -12,6 +12,14 @@ export class ProjectServiceMock implements Required<ProjectService> {
return of([]); return of([]);
} }
getCompletedProjectById(projectId: string): Observable<Project> {
return of();
}
getEvaluatedProjectById(projectId: string): Observable<Project> {
return of();
}
saveProject(saveProject: ProjectDialogBody): Observable<Project> { saveProject(saveProject: ProjectDialogBody): Observable<Project> {
return of(); return of();
} }

View File

@ -5,7 +5,7 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';
import {Project, ProjectDialogBody} from '@shared/models/project.model'; import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {environment} from '../../environments/environment'; import {environment} from '../../../environments/environment';
import {throwError} from 'rxjs'; import {throwError} from 'rxjs';
describe('ProjectService', () => { describe('ProjectService', () => {
@ -83,6 +83,7 @@ describe('ProjectService', () => {
client: 'E Corp', client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning', title: 'Some Mock API (v1.0) Scanning',
tester: 'Novatester', tester: 'Novatester',
summary: ''
}; };
const mockProject: Project = { const mockProject: Project = {

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment'; import {environment} from '../../../environments/environment';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {Project, ProjectDialogBody} from '../models/project.model'; import {Project, ProjectDialogBody} from '../../models/project.model';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
@Injectable({ @Injectable({
@ -21,6 +21,20 @@ export class ProjectService {
return this.http.get<Project[]>(`${this.apiBaseURL}`); return this.http.get<Project[]>(`${this.apiBaseURL}`);
} }
/**
* Get completed project by id
*/
public getCompletedProjectById(projectId: string): Observable<Project> {
return this.http.get<Project>(`${this.apiBaseURL}/${projectId}`);
}
/**
* Get evaluated project by id
*/
public getEvaluatedProjectById(projectId: string): Observable<Project> {
return this.http.get<Project>(`${this.apiBaseURL}/evaluation/${projectId}`);
}
/** /**
* Save Project * Save Project
* @param project the information of the project * @param project the information of the project

View File

@ -0,0 +1,28 @@
import { TestBed } from '@angular/core/testing';
import { ReportingService } from './reporting.service';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {environment} from '../../../environments/environment';
describe('ReportingService', () => {
let service: ReportingService;
let httpMock: HttpTestingController;
const reportBaseURL = `${environment.reportEndpoint}/reports`;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
]
});
service = TestBed.inject(ReportingService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,23 @@
import {Injectable} from '@angular/core';
import {environment} from '../../../environments/environment';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ReportingService {
constructor(private http: HttpClient) {
}
private reportBaseURL = `${environment.reportEndpoint}/reports`;
/**
* Get PDF Report by project id
*/
public getReportPDFforProjectById(projectId: string): Observable<ArrayBuffer> {
// @ts-ignore
return this.http.get<ArrayBuffer>(`${this.reportBaseURL}/${projectId}/pdf`, {responseType: 'arraybuffer'})
}
}

View File

@ -5,10 +5,10 @@ import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpLoaderFactory} from '../../app/common-app.module'; import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store'; import {NgxsModule} from '@ngxs/store';
import {SessionState} from '../stores/session-state/session-state'; import {SessionState} from '../../stores/session-state/session-state';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';
import {NbToastrModule, NbToastrService} from '@nebular/theme'; import {NbToastrModule, NbToastrService} from '@nebular/theme';

View File

@ -4,10 +4,10 @@ import { UserService } from './user.service';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../app/common-app.module'; import {HttpLoaderFactory} from '../../../app/common-app.module';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {NgxsModule} from '@ngxs/store'; import {NgxsModule} from '@ngxs/store';
import {SessionState} from '../stores/session-state/session-state'; import {SessionState} from '../../stores/session-state/session-state';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';
describe('UserService', () => { describe('UserService', () => {

View File

@ -1,6 +1,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http'; import {HttpClient, HttpHeaders} from '@angular/common/http';
import {User} from '../models/user.model'; import {User} from '../../models/user.model';
import {from, Observable, Subscriber} from 'rxjs'; import {from, Observable, Subscriber} from 'rxjs';
import {Store} from '@ngxs/store'; import {Store} from '@ngxs/store';
import {KeycloakService} from 'keycloak-angular'; import {KeycloakService} from 'keycloak-angular';

View File

@ -5,7 +5,7 @@ import {TranslateService} from '@ngx-translate/core';
import {FetchUser, InitSession, ResetSession, UpdateIsAuthenticated, UpdateUser, UpdateUserSettings} from './session-state.actions'; import {FetchUser, InitSession, ResetSession, UpdateIsAuthenticated, UpdateUser, UpdateUserSettings} from './session-state.actions';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import moment from 'moment'; import moment from 'moment';
import {UserService} from '../../services/user.service'; import {UserService} from '../../services/user-service/user.service';
export interface SessionStateModel { export interface SessionStateModel {
userAccount: User; userAccount: User;

View File

@ -54,14 +54,14 @@
"response": [] "response": []
}, },
{ {
"name": "getProjectById", "name": "getCompletedProjectById",
"request": { "request": {
"auth": { "auth": {
"type": "bearer", "type": "bearer",
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzY5NzMxMTAsImlhdCI6MTY3Njk3MjgxMCwianRpIjoiNDFkMDAwNzEtNjAyYy00NmEzLThjYjctMTJlZTExYWYyZDBhIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5MTA5ZWU0Ni03OGEzLTRmMDUtODdhYi03NzIxNGJmNzNlZWMiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiYzRwb191c2VyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImM0cG9fbG9jYWwiOnsicm9sZXMiOlsidXNlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiOTEwOWVlNDYtNzhhMy00ZjA1LTg3YWItNzcyMTRiZjczZWVjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.hZhUBi4cGQdn3lZ1Xm1Kz2WpboiBBJCFrtODD_c4N0ZiymB0MWVc1jXzU1fQ25mZ_I9VJXqg97x_gnCM7mKJrncFxs6cj75zIeH3so1BhlcDf7q2pjIkCH1yCerPWSLtrK2pWyxTr1GyO1Cp_wqQms_7_rmpzajLzmqBGKF8vd4yAk8kHmBoGBJhRU_gVCsDnIe74in3a032---IgCJ2XA0E5yxP9oBe6_9xPuCsk82YDihbfK1ZEO-9YZt0g1Iv3y30-hG10eflftWJEMSi8Bso4H_2WSJLqy4YRuGQR0EKDiomM0deVCK9IkuaoIsdIZ8kd65YuxSnj-_ue17QTA", "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzcwNjI4MTAsImlhdCI6MTY3NzA2MjUxMCwianRpIjoiM2NlNWIyNjEtZGZkNy00NzIwLThlODMtOTczYjg0NTIxZWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiJlYWU4MTBkOS04OWU4LTQxNmEtOGExNS1kYzI5MWY5YmY0NGQiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiYzRwb191c2VyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImM0cG9fbG9jYWwiOnsicm9sZXMiOlsidXNlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiZWFlODEwZDktODllOC00MTZhLThhMTUtZGMyOTFmOWJmNDRkIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.u2NP-PV9uNUtE8IyqOEjwFs_tEF55yU4F0KbdIb8P_1yl_ExiIGFBzEm_n5uUA-3AhXid56RAlrysEGjsTVKWm9c37MGc4oyzZzHoXoBFOquA-mb7K0bazmyFCSLCYszOM6oLcSrKelStu_aoga0MAqL1yrl8qYpZ902O32hG_5BJwEJo0uN-60dcDXwQMed8GqkraFoMCPBQF5vGZrGeNAMGwHXN1__E9JJU8ehPMKV_vnv11HrzK6OJfx4esWkF_5aNi-MASF2vaREbGVM0d0PGfgWDWphtJRK8acc4oHfqzWBW8M2qudZ7FDgMDJgk1mgFfUcQ_TEl-gdO_5PYQ",
"type": "string" "type": "string"
}, },
{ {
@ -87,6 +87,41 @@
}, },
"response": [] "response": []
}, },
{
"name": "getEvaluatedProjectById",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzcwNjI4MTAsImlhdCI6MTY3NzA2MjUxMCwianRpIjoiM2NlNWIyNjEtZGZkNy00NzIwLThlODMtOTczYjg0NTIxZWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiJlYWU4MTBkOS04OWU4LTQxNmEtOGExNS1kYzI5MWY5YmY0NGQiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiYzRwb191c2VyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImM0cG9fbG9jYWwiOnsicm9sZXMiOlsidXNlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwic2lkIjoiZWFlODEwZDktODllOC00MTZhLThhMTUtZGMyOTFmOWJmNDRkIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.u2NP-PV9uNUtE8IyqOEjwFs_tEF55yU4F0KbdIb8P_1yl_ExiIGFBzEm_n5uUA-3AhXid56RAlrysEGjsTVKWm9c37MGc4oyzZzHoXoBFOquA-mb7K0bazmyFCSLCYszOM6oLcSrKelStu_aoga0MAqL1yrl8qYpZ902O32hG_5BJwEJo0uN-60dcDXwQMed8GqkraFoMCPBQF5vGZrGeNAMGwHXN1__E9JJU8ehPMKV_vnv11HrzK6OJfx4esWkF_5aNi-MASF2vaREbGVM0d0PGfgWDWphtJRK8acc4oHfqzWBW8M2qudZ7FDgMDJgk1mgFfUcQ_TEl-gdO_5PYQ",
"type": "string"
},
{
"key": "undefined",
"type": "any"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8443/projects/evaluation/5a4f126c-9471-43b8-80b9-6eb02b7c35d0",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"projects",
"evaluation",
"5a4f126c-9471-43b8-80b9-6eb02b7c35d0"
]
}
},
"response": []
},
{ {
"name": "saveProject", "name": "saveProject",
"request": { "request": {

View File

@ -66,6 +66,20 @@ fun Project.toProjectCompletedPentestResponseBody(): ResponseBody {
) )
} }
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
fun Project.toProjectEvaluatedPentestResponseBody(): ResponseBody {
return mapOf(
"id" to id,
"client" to client,
"title" to title,
"createdAt" to createdAt,
"tester" to tester,
"summary" to summary,
"projectPentests" to projectPentests,
"createdBy" to createdBy
)
}
fun Project.toProjectDeleteResponseBody(): ResponseBody { fun Project.toProjectDeleteResponseBody(): ResponseBody {
return mapOf( return mapOf(
"id" to id "id" to id
@ -76,9 +90,9 @@ fun Project.toProjectDeleteResponseBody(): ResponseBody {
fun Project.calculateProgress(): BigDecimal { fun Project.calculateProgress(): BigDecimal {
// Total number of pentests listet in the OWASP testing guide // Total number of pentests listet in the OWASP testing guide
// https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf // https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf
// @Value("\${owasp.web.pentests}") // @Value("\${owasp.web.objectives}")
// lateinit var TOTALPENTESTS: Int // lateinit var TOTALPENTESTS: Int
val TOTALPENTESTS = 95.0 val TOTAL_OWASP_OBJECTIVES = 95.0
return if (projectPentests.isEmpty()) return if (projectPentests.isEmpty())
BigDecimal.ZERO BigDecimal.ZERO
@ -92,7 +106,7 @@ fun Project.calculateProgress(): BigDecimal {
completedPentests += 0.5 completedPentests += 0.5
} }
} }
val progress = (completedPentests * 100) / TOTALPENTESTS val progress = (completedPentests * 100) / TOTAL_OWASP_OBJECTIVES
BigDecimal(progress).setScale(2, RoundingMode.HALF_UP) BigDecimal(progress).setScale(2, RoundingMode.HALF_UP)
} }
} }

View File

@ -35,7 +35,7 @@ class ProjectController(private val projectService: ProjectService) {
} }
@GetMapping("/{projectId}") @GetMapping("/{projectId}")
fun getProjectById( fun getCompletedProjectById(
@PathVariable(value = "projectId") projectId: String @PathVariable(value = "projectId") projectId: String
): Mono<ResponseEntity<ResponseBody>> { ): Mono<ResponseEntity<ResponseBody>> {
return projectService.getProjectById(projectId).map { return projectService.getProjectById(projectId).map {
@ -46,6 +46,18 @@ class ProjectController(private val projectService: ProjectService) {
} }
} }
@GetMapping("/evaluation/{projectId}")
fun getProjectById(
@PathVariable(value = "projectId") projectId: String
): Mono<ResponseEntity<ResponseBody>> {
return projectService.getProjectById(projectId).map {
it.toProjectEvaluatedPentestResponseBody()
}.map {
if (it.isEmpty()) ResponseEntity.noContent().build()
else ResponseEntity.ok(it)
}
}
@PostMapping @PostMapping
fun saveProject( fun saveProject(
@RequestBody body: ProjectRequestBody @RequestBody body: ProjectRequestBody

View File

@ -22,6 +22,6 @@ keycloakhost=localhost
keycloak.client.url=http://localhost:8080 keycloak.client.url=http://localhost:8080
keycloak.client.realm.path=auth/realms/c4po_realm_local/ keycloak.client.realm.path=auth/realms/c4po_realm_local/
## Total number of pentests listet in the OWASP testing guide ## Total number of pentests / objectives listet in the OWASP testing guide
## https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf ## https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf
owasp.web.pentests=95 owasp.web.objectives=95