Compare commits

...

68 Commits

Author SHA1 Message Date
mhg 661cbe580d feat: upgraded angular version from 12 to 15 2024-05-15 16:34:47 +02:00
mhg 8114abe9b6 feat: As an user I want to have the latest Testing Guide (4.2) information 2024-05-10 12:52:31 +02:00
mhg 1888a98e08 fix: updated readme file contents 2023-09-06 10:53:56 +02:00
Marcel Haag a5fa8ca9dd fix: added data & fixed small bugs 2023-08-30 12:50:27 +02:00
Norman Schmidt 930306d00f fix: Keycloak logout issue 2023-08-25 16:20:44 +02:00
Marcel Haag dee3c35180 feat: As a developer I want to upload my docker images to DockerHub 2023-08-11 14:18:56 +02:00
Cel 306ee25085
Update README.md 2023-06-14 14:43:11 +02:00
Marcel Haag 7055b7caf5 feat: As a developer I need a pipeline using github actions for all services 2023-06-07 11:02:49 +02:00
Marcel Haag 056e4532fa feat: As an user I want to see only the projects I created 2023-06-05 10:43:56 +02:00
Marcel Haag b6ec78ef49 feat: Adjust keycloak flow for reset password, make email required 2023-05-31 10:47:09 +02:00
Marcel Haag 2f6fd7c2bc feat: Secure MongoDB and add Liquibase to project 2023-05-22 15:10:30 +02:00
Marcel Haag bc8d59f1a9 feat: Create a tutorial dialog 2023-05-10 14:42:22 +02:00
Marcel Haag cf7204fb9d feat: As a user, I want to create reports only in PDF file format 2023-05-09 09:45:45 +02:00
Marcel Haag 6e55e61ce5 feat: As a user I want the japser report to support english and german 2023-05-05 10:31:36 +02:00
Marcel Haag 061b93ff5e feat: include reporting service in docker-compose 2023-05-03 15:08:18 +02:00
Marcel Haag 65985e5ef3 fix: error messages for user profile dialog 2023-04-24 11:42:23 +02:00
Marcel Haag ed7c70c62d fix: keycloak realm issues 2023-04-21 16:21:14 +02:00
Marcel Haag b4bf6de8f8 feat: As a tester I want to edit my profile 2023-04-19 15:27:24 +02:00
Marcel Haag e0e23f7383 feat: As a user I want to disable / enable objectives 2023-04-12 14:26:58 +02:00
Marcel Haag 07c6871294 feat: As a user I want to add the project version 2023-04-10 17:01:23 +02:00
Marcel Haag 9e4fa27b92 feat: As an user I want to add the pentest status of a project 2023-04-05 18:32:26 +02:00
Marcel Haag 2c7ac85f6e feat: As an user I want a retry dialog guard, in order to resend a failed request 2023-03-29 21:58:29 +02:00
Marcel Haag 9fddff7740 fix: As a user I want to remove all related the findings and comments after deleting a project 2023-03-24 13:25:41 +01:00
Marcel Haag a37e06f8ca feat: As a user I want to see my profile in the header and log myself out manually 2023-03-08 14:43:41 +01:00
Marcel Haag 3be43fa96e feat: As a user I want a timer to track the time spent on each objective 2023-03-08 11:04:49 +01:00
Marcel Haag 4ca2828e0f fix: As a user I want to refactor the current routing logic 2023-02-25 17:50:00 +01:00
Marcel Haag 79a2493c37 feat: As a user I want to have an export dialog to download my pentest report 2023-02-23 16:08:01 +01:00
Marcel Haag bb544c71a0 feat: As a user I want to get project data by projectId that is filled with all the necessary data for report creation 2023-02-21 15:29:13 +01:00
Marcel Haag 280948c470 feat: As a developer, I want to have spring security added to the reporting microservice 2023-02-15 14:30:27 +01:00
Marcel Haag 88b3647295 feat: As a user I want to add a summary to the project when editing on the main page & when inside a project 2023-02-13 14:55:33 +01:00
Marcel Haag 3c3f005537 feat: As a developer I want an additional microservice for creating pentest reports 2023-02-13 11:28:33 +01:00
nsm e27d6005db feat: start C4PO with docker-compose 2023-01-17 10:02:21 +01:00
Norman Schmidt 40224635ab feat: upgrade Keycloak to version 20 2023-01-17 10:02:21 +01:00
Marcel Haag cc182d932b As a use I want to have a saver delete dialog 2023-01-03 10:56:25 +01:00
Marcel Haag f5e34722f5 feat: As a user I want to edit my comment 2022-12-28 15:56:53 +01:00
Marcel Haag 6f209dfbb4 feat: As a user I want to delete my comment 2022-12-23 13:45:13 +01:00
Marcel Haag 16bc1a5d4f fix: design pattern and QoL Improvements 2022-12-23 13:07:17 +01:00
Marcel Haag 46f79dcf89 feat: As a user I want to add a comment via dialog 2022-12-22 15:47:37 +01:00
Marcel Haag 6a625349c8 feat: As a user I want to delete my finding 2022-12-22 15:47:37 +01:00
Marcel Haag 27a8e963e9 feat: As a user I want to edit my finding 2022-12-08 10:20:05 +01:00
Marcel Haag 076fa087e8 fix: As a developer I want tests for the new API's and document them 2022-12-02 17:13:10 +01:00
Marcel Haag a4536e9735 fix: Bugfixes and QoL Improvements 2022-11-21 09:56:13 +01:00
Marcel Haag e9aec4ec3e feat: As a developer, I want to get findings by pentestId 2022-11-16 09:26:27 +01:00
Marcel Haag b2f430f9fd feat: As a developer I want to create a finding that is related to my pentest 2022-11-14 12:33:33 +01:00
Marcel Haag 84b7c1a07d feat: As a developer, I want to create a pentest 2022-11-11 19:02:35 +01:00
Marcel Haag de659e3293 feat: added pentest status selection 2022-11-04 12:49:38 +01:00
Marcel Haag c1293b4da1 fix: dialog lazy loading 2022-11-04 11:46:44 +01:00
Marcel Haag f658073bcf feat: As a user I want to add a finding via dialog 2022-11-02 10:49:10 +01:00
Marcel Haag 5d89467c1e feat: As a user I want to have an comments overview 2022-10-24 15:33:20 +02:00
Marcel Haag 747cade495 feat: As a user I want to have an findings overview 2022-10-24 13:18:36 +02:00
Marcel Haag 6764583481 feat: As a user I want to view the information about my current pentest 2022-10-14 11:38:35 +02:00
Marcel Haag ca7018cad9 fix: Changed c4po logo and refactored FE 2022-09-23 11:02:15 +02:00
Marcel Haag b695b17a9e feat: As an user I want to have the layout for my pentest 2022-09-01 17:28:24 +02:00
Marcel Haag ecaaeea079 fix: integration test and ascii documentation 2022-08-19 13:14:59 +02:00
mhg d814a87033 Create THIRD-PARTY-LICENSES.md
This is only a template and should be updated before making the repository public.
2022-08-19 13:14:59 +02:00
mhg 5894722633 Updated LICENSE.md 2022-08-19 13:14:59 +02:00
mhg 93ccf3e034 Delete LICENSE.md 2022-08-19 13:14:59 +02:00
mhg 6ae4d4e62d Update CONTRIBUTING.md 2022-08-19 13:14:59 +02:00
mhg 2cbb2d522b Create CONTRIBUTING.md 2022-08-19 13:14:59 +02:00
mhg 6ec16c0cbf Create LICENSE.md 2022-08-19 13:14:59 +02:00
mhg 4a2024dd69 feat: refactor project model in order to calculate progress 2022-08-19 13:14:59 +02:00
norman-schmidt 24dccb3e8f
feat: added pentest endpoint to get pentests by projectId & category 2022-08-05 11:00:15 +02:00
mhg ca77f6f208 feat: update jest version and adjusted wiki 2022-07-26 08:59:30 +02:00
mhg e80c0e8560
feat: As a developer I want to have a global and customizable exception handeling 2022-07-05 20:10:29 +02:00
mhg f9ce7606f7
feat: As an user I want to have an additional pentest-header 2022-06-24 22:00:07 +02:00
Stipe Knez abddd00451 feat: As a user I want to have a sidenav in order to select the current step of my Pentest
Co-authored-by: Marcel Haag <marcel.haag@novatec-gmbh.de>
2022-06-15 08:44:32 +02:00
mhg 5d5dbe95fa
feat: added pentest table and added ngrx store for project management 2022-04-21 15:09:44 +02:00
norman-schmidt 501a6d3427
feat: switch language 2022-04-04 00:42:14 +02:00
483 changed files with 70652 additions and 12531 deletions

106
.github/workflows/c4po-ci.yml vendored Normal file
View File

@ -0,0 +1,106 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: "CI: Clean Build C4PO"
on:
pull_request:
branches: [ "main" ]
env:
ANGULAR_PATH: security-c4po-angular
API_PATH: security-c4po-api
REPORTING_PATH: security-c4po-reporting
CFG_PATH: security-c4po-cfg
ANGULAR_CLI_VERSION: 15
jobs:
angular_job:
name: "Angular Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Use Node.js 16.x"
uses: actions/setup-node@v1
with:
node-version: '16.x'
cache: 'npm'
- name: "Install NPM dependencies"
run: |
cd $ANGULAR_PATH
npm ci
- name: "Build assets"
run: |
cd $ANGULAR_PATH
npm run build --if-present
- name: "Run tests"
run: |
cd $ANGULAR_PATH
npm test
api_job:
name: "API Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Set up JDK 11"
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.5
- name: "Execute Gradle build"
run: |
cd $API_PATH
./gradlew clean build -x dependencyCheckAnalyze
reporting_job:
name: "Reporting Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Set up JDK 11"
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.5
- name: "Execute Gradle build"
run: |
cd $REPORTING_PATH
./gradlew clean build

173
.github/workflows/c4po-release.yml vendored Normal file
View File

@ -0,0 +1,173 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: "CD: Publish C4PO to Docker Hub"
on:
push:
branches: [ "main" ]
env:
ANGULAR_PATH: security-c4po-angular
API_PATH: security-c4po-api
REPORTING_PATH: security-c4po-reporting
CFG_PATH: security-c4po-cfg
jobs:
angular_job:
name: "Angular Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Use Node.js 16.x"
uses: actions/setup-node@v1
with:
node-version: '16.x'
cache: 'npm'
- name: "Install NPM dependencies"
run: |
cd $ANGULAR_PATH
npm ci
- name: "Build assets"
run: |
cd $ANGULAR_PATH
npm run build --if-present
- name: "Run tests"
run: |
cd $ANGULAR_PATH
npm test
api_job:
name: "API Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Set up JDK 11"
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.5
- name: "Execute Gradle build"
run: |
cd $API_PATH
./gradlew clean bootJar -x dependencyCheckAnalyze
- uses: actions/upload-artifact@v3
with:
name: API-jar
path: security-c4po-api/build/libs/
reporting_job:
name: "Reporting Job"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: "Set up JDK 11"
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
with:
gradle-version: 6.5
- name: "Execute Gradle build"
run: |
cd $REPORTING_PATH
./gradlew clean bootJar
- uses: actions/upload-artifact@v3
with:
name: REPORTING-jar
path: security-c4po-reporting/build/libs/
push_c4po_to_docker_hub:
name: "Push images to Docker Hub"
runs-on: ubuntu-latest
needs: [angular_job, api_job, reporting_job]
steps:
- name: "Check out the repo"
uses: actions/checkout@v3
- name: "Log in to Docker Hub"
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "Extract metadata (tags, labels) for Docker"
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: cellecram/security-c4po # my-docker-hub-namespace/my-docker-hub-repository
- name: Download jar api artifact
uses: actions/download-artifact@v3
with:
name: API-jar
path: security-c4po-api/build/libs/
- name: Download jar reporting artifact
uses: actions/download-artifact@v3
with:
name: REPORTING-jar
path: security-c4po-reporting/build/libs/
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 #v1
- name: "Buildx & Push Docker images for AMD64 & ARM64"
run: |
cd $CFG_PATH
docker buildx build --push \
--platform linux/amd64,linux/arm64 \
--tag cellecram/security-c4po:mongo ./c4po-db
docker buildx build --push \
--platform linux/amd64,linux/arm64 \
--tag cellecram/security-c4po:keycloak ./c4po-keycloak
docker buildx build --push \
--build-arg JAR_FILE_REPORT=./build/libs/security-c4po-reporting-0.0.1-SNAPSHOT.jar \
--build-arg SPRING_PROFILES_ACTIVE=COMPOSE \
--platform linux/amd64,linux/arm64 \
--tag cellecram/security-c4po:reporting ../security-c4po-reporting
docker buildx build --push \
--build-arg JAR_FILE_API=./build/libs/security-c4po-api-0.0.1-SNAPSHOT.jar \
--build-arg SPRING_PROFILES_ACTIVE=COMPOSE \
--platform linux/amd64,linux/arm64 \
--tag cellecram/security-c4po:api ../security-c4po-api
docker buildx build --push \
--platform linux/amd64,linux/arm64 \
--tag cellecram/security-c4po:angular ../security-c4po-angular

56
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,56 @@
# Contributing to Security-C4PO
First off, thanks for taking the time to contribute! 👍
The following is a set of guidelines for contributing to this project and its packages, which are hosted on GitHub.
These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
## How Can I Contribute?
### Reporting Bugs
This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report.
Explain the problem and include additional details to help maintainers reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started the application, e.g. which command exactly you used in the terminal, or how you started the application otherwise. When listing steps, **don't just say what you did, but explain how you did it**.
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion, including completely new features and minor improvements to existing functionality.
Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.
* **Use a clear and descriptive title** for the issue to identify the suggestion.
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
* **Include screenshots, mock-ups or animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to.
* **Explain why this enhancement would be useful**
## Code of Conduct
Use the following conventions:
* Branch: `<initial>_c4po_<issuenumber>`
* Commit: `feat: <What was implemented?>` or `fix: <What got fixed?>`
By participating, you are expected to uphold this code.
## Local development
Security-C4PO and all it's included micorservices can be developed locally.
Execute `c4po-dev.sh` and all services will run on a dev server.
#### Testuser Credentials:
* Username: c4po
* Password: Test1234!
#### Technical Environment Requirements
* Docker / Docker-compose
* OpenJDK 11
* Node 14.15.1 / npm 6.14.8
#### Helpfull Tools
* mongoDB Compass
* Postman
## Issue Board
[C4PO Board](https://github.com/Marcel-Haag/security-c4po/projects/1)

201
LICENSE.md Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2020] [Marcel Haag]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,40 +1,94 @@
# security-c4po
![workflow_badge](https://github.com/Marcel-Haag/security-c4po/actions/workflows/c4po-ci.yml/badge.svg?branch=main)
![workflow_badge](https://github.com/Marcel-Haag/security-c4po/actions/workflows/c4po-release.yml/badge.svg?branch=main)
[![OWASP Incubator](https://img.shields.io/badge/owasp-incubator%20project-3267fe.svg)](https://owasp.org/other_projects/)<!-- @IGNORE PREVIOUS: link -->
### Chief Innovator
> Daniel Mader
### Project Leads
* Andreas Falk
* Christina Paule
![alt architecture](./wiki/repository-owasp-guide-c4po.png)
### Developers
* Marcel Haag
* Norman Schmidt
* Stipe Knez
Welcome to the frontend repository of Security C4PO, an open-source pentest reporting tool.
Security C4PO is a powerful, user-friendly tool designed to simplify the process of generating professional pentest reports.
It aims to streamline and automate the often time-consuming task of creating comprehensive reports by providing an intuitive web-based interface that facilitates the content of the [OWASP TESTING GUIDE](https://owasp.org/www-project-web-security-testing-guide/v42/).
This repository contains the codebase of Security C4PO, built with an Angular Frontend and two Spring Boot Backend Microservices.
[![YouTube](https://img.shields.io/badge/YouTube-%23FF0000.svg?style=for-the-badge&logo=YouTube&logoColor=white)](https://www.youtube.com/channel/UCDwRRDVepRUowI0NmBy_9lQ)
## Table of Contents
* [Docker Hub Setup](#docker-hub-setup)
* [Application Architecture](#application-architecture)
* [Data Structure](#data-structure)
* [C4PO Roadmap](#c4po-roadmap)
* [Project](#project)
* [Technical Requirements](#technical-requirements)
* [Tools](#tools)
* [Conventions](#conventions)
* [Development server](#development-server)
* [Testuser Credentials](#testuser-credentials)
* [Contributing](#contributing)
* [License](#license)
## Docker Hub Setup
[![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://hub.docker.com/repository/docker/cellecram/security-c4po/general)
* Pull all images:
* `docker image pull --all-tags cellecram/security-c4po`
* Create network:
* `docker network create -d bridge c4po`
* Start images:
* `docker run --network=c4po --name c4po-keycloak -d -p 8080:8080 cellecram/security-c4po:keycloak`
* `docker run --network=c4po --name c4po-db -d -p 27017:27017 cellecram/security-c4po:mongo`
* `docker run --network=c4po --name c4po-angular -d -p 4200:4200 cellecram/security-c4po:angular`
* `docker run --network=c4po -e "SPRING_PROFILES_ACTIVE=COMPOSE" --name c4po-api -d -p 8443:8443 cellecram/security-c4po:api`
* `docker run --network=c4po -e "SPRING_PROFILES_ACTIVE=COMPOSE" --name c4po-reporting -d -p 8444:8444 cellecram/security-c4po:reporting`
### OR: Run Script (Docker Hub)
Execute `c4po-prod.sh` and all services will be pulled from Docker Hub and started.
You can reach the application by entering http://localhost:4200 in you browser.
## Application Architecture
![alt architecture](./wiki/C4PO-Architecture.png)
## Data Structure
![alt datastructure](./wiki/C4PO-Datastructure.png)
## C4PO Roadmap
![alt roadmap](./wiki/C4PO-Roadmap.png)
## Project
![Angular](https://img.shields.io/badge/angular-%23DD0031.svg?style=for-the-badge&logo=angular&logoColor=white)
![RxJS](https://img.shields.io/badge/rxjs-%23B7178C.svg?style=for-the-badge&logo=reactivex&logoColor=white)
![Spring](https://img.shields.io/badge/spring-%236DB33F.svg?style=for-the-badge&logo=spring&logoColor=white)
![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white)
![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white)
### Technical Requirements
* Docker / Docker-compose
* OpenJDK 11
* Node 14.15.1 / npm 6.14.8
* Node 16.20.2 / npm 8.19.4
* MongoDB 4.4.6
### Tools
* mongoDB Compass
* Postman
## Application Architecture
![alt architecture](./wiki/SecurityC4PO_Architecture.png)
## Data Structure
![alt architecture](./wiki/SecurityC4PO_Data_Structure.png)
* Jaspersoft Studio
### Conventions
* Branch: `<initial>_c4po_<issuenumber>`
* Commit: `feat: <What was implemented?>` or `fix: <What got fixed?>`
### Development server
Execute 'c4po.sh' and all services will run on a dev server.
Execute `c4po-dev.sh` and all services will run on a dev server.
You can reach the application by entering http://localhost:4200 in you browser.
### Testuser Credentials:
* Username: ttt
### Testuser Credentials
* Username: c4po
* Password: Test1234!
## Contributing
Contributions to Security C4PO are welcome! If you'd like to contribute to the project, please follow the guidelines outlined in the [CONTRIBUTING.md](https://github.com/marcel-haag/security-c4po/blob/main/CONTRIBUTING.md) file.
## License
Security C4PO is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) License. Please see the [LICENSE](https://github.com/marcel-haag/security-c4po/blob/main/LICENSE.md) file for more information.
We hope you find Security C4PO useful for managing and generating pentest reports. If you encounter any issues or have suggestions for improvement, please feel free to create an issue on the [issue tracker](https://github.com/Marcel-Haag/security-c4po/issues).

13
THIRD-PARTY-LICENSES.md Normal file
View File

@ -0,0 +1,13 @@
## Dependency License Report for security-c4po SNAPSHOT
#### Example
1. Group: antlr Name: antlr Version: 2.7.7
POM Project URL: http://www.antlr.org/
POM License: BSD License - http://www.antlr.org/license.html
--------------------------------------------------------------------------------
This report was generated at Thu Oct 01 09:28:53 CEST 2020.

View File

@ -1,15 +1,5 @@
#!/bin/bash
baseDir=$(pwd)
composeDir=$baseDir"/security-c4po-cfg"
keycloakVolume="security-c4po-cfg/volumes/keycloak/data/*"
mongoVolume="security-c4po-cfg/volumes/mongodb/data/*"
composeKeycloak=$baseDir"/security-c4po-cfg/kc/docker-compose.keycloak.yml"
composeDatabase=$baseDir"/security-c4po-cfg/mongodb/docker-compose.mongodb.yml"
composeFrontend=$baseDir"/security-c4po-cfg/frontend/docker-compose.frontend.yml"
composeBackend=$baseDir"/security-c4po-cfg/backend/docker-compose.backend.yml"
compose=$baseDir"/security-c4po-cfg/docker-compose.yml"
echo -e "
@ -24,24 +14,29 @@ ______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/
echo "-------------CLEAN UP Container---------------"
echo -e "\n"
rm -r ${keycloakVolume}
docker rm -f c4po-keycloak
docker rm -f c4po-keycloak-postgres
docker rm -f c4po-db
#docker rm -f c4po-keycloak ### toggle to clear keycloak with every start ###
#docker rm -f c4po-db ### toggle to clear database with every start ###
docker rm -f c4po-reporting
docker rm -f c4po-api
docker rm -f c4po-angular
echo -e "\n"
echo "-----------------Start Build------------------"
echo " - Report Engine: "
docker-compose -f ${compose} build c4po-db
echo " - Report Engine: "
docker-compose -f ${compose} build c4po-keycloak
echo -e "\n"
echo " - Report Engine: "
docker-compose -f ${compose} build c4po-reporting --build-arg JAR_FILE_REPORT=./build/libs/security-c4po-reporting-0.0.1-SNAPSHOT.jar ### toggle for additional build args ###
echo -e "\n"
echo " - Backend: "
docker-compose -f ${composeBackend} build
docker-compose -f ${compose} build c4po-api --build-arg JAR_FILE_API=./build/libs/security-c4po-api-0.0.1-SNAPSHOT.jar ### toggle for additional build args ###
echo -e "\n"
echo " - Frontend: "
docker-compose -f ${composeFrontend} build
docker-compose -f ${compose} build c4po-angular
echo -e "\n"
# docker-compose -f ${compose} up
echo "------------Start Docker Container------------"
echo -e "\n"
docker-compose -f ${composeKeycloak} -f ${composeDatabase} -f ${composeBackend} -f ${composeFrontend} up
# docker-compose -f ${compose} up
docker-compose -f ${compose} up

35
c4po-prod.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
baseDir=$(pwd)
compose=$baseDir"/security-c4po-cfg/docker-compose.yml"
echo -e "
_______ _______ _______ _ _ ______ _____ _______ __ __
|______ |______ | | | |_____/ | | \_/
______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/ _/ _/_/_/ _/_/
_/ _/ _/ _/ _/ _/ _/
_/ _/_/_/_/ _/_/_/ _/ _/
_/ _/ _/ _/ _/
_/_/_/ _/ _/ _/_/
\n"
echo "---------------Pull C4PO from Docker Hub----------------"
echo -e "\n"
docker image pull --all-tags cellecram/security-c4po
echo -e "\n"
echo "---------------Create Network----------------"
echo -e "\n"
docker network create -d bridge c4po
echo -e "\n"
echo "---------------Start Containers---------------"
echo -e "\n"
docker run --network=c4po --name c4po-keycloak -d -p 8080:8080 cellecram/security-c4po:keycloak
echo -e "\n"
docker run --network=c4po --name c4po-db -d -p 27017:27017 cellecram/security-c4po:mongo
echo -e "\n"
docker run --network=c4po --name c4po-angular -d -p 4200:4200 cellecram/security-c4po:angular
echo -e "\n"
docker run --network=c4po -e "SPRING_PROFILES_ACTIVE=COMPOSE" --name c4po-api -d -p 8443:8443 cellecram/security-c4po:api
echo -e "\n"
docker run --network=c4po -e "SPRING_PROFILES_ACTIVE=COMPOSE" --name c4po-reporting -d -p 8444:8444 cellecram/security-c4po:reporting

View File

@ -1,18 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -44,3 +44,6 @@ testem.log
# System Files
.DS_Store
Thumbs.db
# Chache
.angular/*

View File

@ -1,5 +1,5 @@
# base image
FROM node:12.13.1
FROM node:14
# set working directory
WORKDIR /app
@ -9,8 +9,8 @@ ENV PATH /app/node_modules/.bin:$PATH
# install and cache app dependencies
COPY package.json /app/package.json
RUN npm install
RUN npm install -g @angular/cli@10.2.0
RUN NODE_ENV=development npm install
RUN NODE_ENV=development npm install -g @angular/cli@12.2.17
# add app
COPY . /app

View File

@ -1,6 +1,6 @@
# SecurityC4poAngular
# Security C4PO Angular
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
This Angular application serves as the frontend interface for Security C4PO, allowing users to efficiently manage and generate comprehensive reports for their penetration testing activities.
## Development server
@ -16,12 +16,19 @@ Run `ng build` to build the project. The build artifacts will be stored in the `
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
Run `ng test` to execute the unit tests via [Jest](https://jestjs.io/).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Run `ng e2e` to execute the end-to-end tests via [Cypress](https://www.cypress.io/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
## Contributing
Pull requests are welcome. For major changes, please open an issue first
to discuss what you would like to change.
Please make sure to read our [contributing guideline](https://github.com/marcel-haag/security-c4po/blob/main/CONTRIBUTING.md).

View File

@ -22,25 +22,37 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/assets/images/favicons/favicon.ico",
"src/assets/images/favicons/corporate_favicon.ico",
"src/assets"
],
"styles": [
"src/assets/@theme/styles/styles.scss"
"src/assets/@theme/styles/styles.scss",
"node_modules/@fortawesome/fontawesome-free/css/all.css",
"node_modules/@glidejs/glide/src/assets/sass/glide.core.scss",
"node_modules/@glidejs/glide/src/assets/sass/glide.theme.scss"
],
"scripts": [
"node_modules/@fortawesome/fontawesome-free/js/all.js"
],
"scripts": [],
"allowedCommonJsDependencies": [
"buffer",
"crypto-js/hmac-sha256",
"crypto-js/lib-typedarrays",
"js-cookie",
"chartjs-plugin-annotation",
"chart.js",
"deep-equal",
"moment-timezone",
"uuid"
]
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@ -53,25 +65,32 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "5mb"
"maximumWarning": "5mb",
"maximumError": "8mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@ -80,7 +99,7 @@
},
"configurations": {
"production": {
"browserTarget": "security-c4po-angular:build:production"
"browserTarget": "security-c4po-angular:build:development"
}
}
},
@ -93,7 +112,9 @@
"test": {
"builder": "@angular-builders/jest:run",
"options": {
"polyfills": "src/polyfills.ts",
"polyfills": [
"src/polyfills.ts"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/assets/images/favicons/favicon.ico",
@ -134,7 +155,6 @@
}
}
},
"defaultProject": "security-c4po-angular",
"cli": {
"analytics": false
}

View File

@ -2,7 +2,8 @@ module.exports = {
moduleNameMapper: {
'@core/(.*)': '<rootDir>/src/app/core/$1',
'@assets/(.*)': '<rootDir>/src/assets/$1',
'@shared/(.*)': '<rootDir>/src/shared/$1'
'@shared/(.*)': '<rootDir>/src/shared/$1',
"^uuid$": "uuid"
},
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],

File diff suppressed because it is too large Load Diff

View File

@ -11,62 +11,72 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^11.0.3",
"@angular/cdk": "^10.2.7",
"@angular/common": "^10.2.3",
"@angular/compiler": "~10.2.0",
"@angular/core": "~10.2.0",
"@angular/flex-layout": "^11.0.0-beta.33",
"@angular/forms": "~10.2.0",
"@angular/localize": "^11.0.2",
"@angular/platform-browser": "~10.2.0",
"@angular/platform-browser-dynamic": "~10.2.0",
"@angular/router": "~10.2.0",
"@briebug/jest-schematic": "^3.0.0",
"@fortawesome/angular-fontawesome": "^0.8.0",
"@fortawesome/fontawesome-common-types": "^0.2.32",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@nebular/eva-icons": "^6.2.1",
"@nebular/theme": "^6.2.1",
"@ngx-translate/core": "^13.0.0",
"@angular/animations": "^15.2.10",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.2.10",
"@angular/compiler": "^15.2.10",
"@angular/core": "^15.2.10",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.10",
"@angular/localize": "^15.2.10",
"@angular/platform-browser": "^15.2.10",
"@angular/platform-browser-dynamic": "^15.2.10",
"@angular/router": "^15.2.10",
"@fortawesome/angular-fontawesome": "^0.10.0",
"@fortawesome/fontawesome-common-types": "^0.2.36",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@glidejs/glide": "^3.6.0",
"@nebular/eva-icons": "^11.0.1",
"@nebular/theme": "^11.0.1",
"@ngneat/until-destroy": "^9.2.3",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@ngxs/store": "^3.7.0",
"@ngxs/storage-plugin": "^3.7.3",
"@ngxs/store": "^3.7.3",
"chart.js": "^4.2.1",
"deep-equal": "^2.0.5",
"eva-icons": "^1.1.3",
"i18n-iso-countries": "^6.2.2",
"font-awesome": "^4.7.0",
"i18n-iso-countries": "^6.8.0",
"jwt-decode": "^3.1.2",
"keycloak-angular": "^8.1.0",
"keycloak-js": "^13.0.0",
"keycloak-angular": "^13.1.0",
"keycloak-js": "^18.0.0",
"moment": "^2.29.1",
"moment-timezone": "latest",
"ng-mocks": "^14.12.2",
"ngx-glide": "^15.0.0",
"ngx-moment": "^5.0.0",
"ngx-take-until-destroy": "^5.4.0",
"ngx-translate-testing": "^5.0.0",
"ngx-translate-testing": "^6.0.0",
"roboto-fontface": "^0.10.0",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"rxjs": "^7.8.0",
"tslib": "^2.3.1",
"uuid": "^8.3.1",
"zone.js": "~0.10.2"
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-builders/jest": "10.0.1",
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.0",
"@schematics/angular": "~10.2.0",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/jest": "26.0.15",
"@types/node": "^12.20.33",
"codelyzer": "^6.0.0",
"font-awesome": "^4.7.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"jest": "26.6.1",
"protractor": "~7.0.0",
"@angular-builders/jest": "^15.0.0",
"@angular-devkit/build-angular": "^15.2.11",
"@angular/cli": "^15.2.11",
"@angular/compiler-cli": "^15.2.10",
"@babel/preset-typescript": "^7.18.6",
"@briebug/jest-schematic": "^2.1.1",
"@fortawesome/fontawesome-free": "^6.4.0",
"@schematics/angular": "^10.2.4",
"@types/jest": "28.1.1",
"@types/node": "^12.20.47",
"codelyzer": "^6.0.2",
"jest": "28.1.1",
"protractor": "^7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
}
"typescript": "~4.9.5"
},
"resolutions": {
"webpack": "^5.0.0"
},
"moduleDirectories": [
"src"
]
}

View File

@ -30,5 +30,23 @@ Object.defineProperty(document.body.style, 'transform', {
},
});
Object.defineProperty(document.body.style, 'transformIgnorePatterns', {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
Object.defineProperty(document.body.style, 'moduleNameMapper', {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
/* output shorter and more meaningful Zone error stack traces */
// Error.stackTraceLimit = 2;

View File

@ -2,20 +2,31 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './home/home.component';
import {AuthGuardService} from '../shared/guards/auth-guard.service';
import {Route} from '@shared/models/route.enum';
export const START_PAGE = 'projects';
export const START_PAGE = Route.PROJECT_OVERVIEW;
const routes: Routes = [
{
path: 'home',
path: Route.HOME,
component: HomeComponent,
canActivate: [AuthGuardService]
},
{
path: 'projects',
path: Route.PROJECT_OVERVIEW,
loadChildren: () => import('./project-overview').then(mod => mod.ProjectOverviewModule),
canActivate: [AuthGuardService]
},
{
path: Route.OBJECTIVE_OVERVIEW,
loadChildren: () => import('./project-overview/project').then(mod => mod.ProjectModule),
canActivate: [AuthGuardService]
},
{
path: Route.PENTEST_OBJECTIVE,
loadChildren: () => import('./pentest').then(mod => mod.PentestModule),
canActivate: [AuthGuardService]
},
// ToDo: Remove after default Keycloak login mask got reworked
/*{
path: 'login',
@ -27,7 +38,7 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, {})],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -1,6 +1,7 @@
<nb-layout>
<!--ToDo: add '*ngIf="$authState.getValue()"' after session store works again-->
<nb-layout-header *ngIf="$authState.getValue()">
<app-header></app-header>
<app-header class="header"></app-header>
</nb-layout-header>
<nb-layout-column>

View File

@ -1,5 +1,9 @@
@import "../assets/@theme/styles/_variables.scss";
.header {
flex: 1;
}
.content-container {
width: 100%;
height: calc(100% - #{$header-height});

View File

@ -2,7 +2,6 @@ import {TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
import {NbLayoutModule, NbThemeModule} from '@nebular/theme';
import {NbEvaIconsModule} from '@nebular/eva-icons';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from './common-app.module';
import {HttpClient} from '@angular/common/http';
@ -27,7 +26,6 @@ describe('AppComponent', () => {
deps: [HttpClient]
}
}),
NbEvaIconsModule,
ThemeModule,
HeaderModule,
NgxsModule.forRoot([SessionState]),

View File

@ -5,11 +5,14 @@ import {registerLocale} from 'i18n-iso-countries';
import {registerLocaleData} from '@angular/common';
import {Store} from '@ngxs/store';
import {BehaviorSubject, Subscription} from 'rxjs';
import {SessionState, SessionStateModel} from '../shared/stores/session-state/session-state';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {filter} from 'rxjs/operators';
import {NbIconLibraries} from '@nebular/theme';
import {FaIconLibrary} from '@fortawesome/angular-fontawesome';
@UntilDestroy()
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@ -23,6 +26,8 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(private translateService: TranslateService,
private store: Store,
private iconLibraries: FaIconLibrary,
private nebularIconLibraries: NbIconLibraries,
@Inject(LOCALE_ID) private localeId: string) {
this.initApp();
}
@ -43,10 +48,14 @@ export class AppComponent implements OnInit, OnDestroy {
initApp(): void {
// for global language
this.translateService.use(this.localeId);
// for number, date and time
registerLocaleData(localeDe, 'de-DE');
// for font-awesome icons
this.nebularIconLibraries.registerFontPack('fas', { packClass: 'fas', iconClassPrefix: 'fa' });
this.nebularIconLibraries.registerFontPack('far', { packClass: 'far', iconClassPrefix: 'fa' });
this.nebularIconLibraries.registerFontPack('fab', { packClass: 'fab', iconClassPrefix: 'fa' });
this.nebularIconLibraries.setDefaultPack('far');
// for country codes
this.setupCountryCode();
}

View File

@ -6,29 +6,37 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {
NbLayoutModule,
NbToastrModule,
NbIconModule, NbCardModule, NbButtonModule, NbDialogService, NbDialogModule,
NbIconModule,
NbCardModule,
NbButtonModule,
NbSelectModule,
NbThemeModule,
NbOverlayContainerAdapter,
NbDialogModule, NbMenuModule, NbIconLibraries,
} from '@nebular/theme';
import {NbEvaIconsModule} from '@nebular/eva-icons';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpLoaderFactory} from './common-app.module';
import {RouterModule} from '@angular/router';
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {fas} from '@fortawesome/free-solid-svg-icons';
import {far} from '@fortawesome/free-regular-svg-icons';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
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 {HeaderModule} from './header/header.module';
import {HomeModule} from './home/home.module';
import {KeycloakService} from 'keycloak-angular';
import {httpInterceptorProviders} from '@shared/interceptors';
import {FlexLayoutModule} from '@angular/flex-layout';
import {NgxsLoggerPluginModule} from '@shared/stores/plugins/store-logger-plugin';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {CustomOverlayContainer} from '@shared/modules/custom-overlay-container.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ConfirmDialogModule} from '@shared/modules/confirm-dialog/confirm-dialog.module';
import {OverlayContainer} from '@angular/cdk/overlay';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.module';
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {fas} from '@fortawesome/free-solid-svg-icons';
import {far} from '@fortawesome/free-regular-svg-icons';
@NgModule({
declarations: [
@ -39,17 +47,22 @@ import {OverlayContainer} from '@angular/cdk/overlay';
AppRoutingModule,
RouterModule,
NbLayoutModule,
NbDialogModule.forRoot(),
NbCardModule,
NbIconModule,
NbButtonModule,
NbDialogModule.forRoot(),
NbThemeModule.forRoot(),
NbToastrModule.forRoot(), // used for notification service
FlexLayoutModule,
ReactiveFormsModule,
FormsModule,
FontAwesomeModule,
BrowserAnimationsModule,
ThemeModule.forRoot(),
NbEvaIconsModule,
ConfirmDialogModule,
NgxsModule.forRoot([SessionState], {developmentMode: !environment.production}),
NbMenuModule.forRoot(),
NbSelectModule,
NgxsModule.forRoot([SessionState, ProjectState], {developmentMode: !environment.production}),
NgxsLoggerPluginModule.forRoot({developmentMode: !environment.production}),
HttpClientModule,
TranslateModule.forRoot({
loader: {
@ -60,7 +73,7 @@ import {OverlayContainer} from '@angular/cdk/overlay';
}),
HeaderModule,
HomeModule,
FlexLayoutModule
RetryDialogModule
],
providers: [
HttpClient,
@ -70,21 +83,22 @@ import {OverlayContainer} from '@angular/cdk/overlay';
multi: true,
deps: [KeycloakService]
},
OverlayContainer,
KeycloakService,
httpInterceptorProviders,
NotificationService,
DialogService,
NbDialogService,
{provide: NbOverlayContainerAdapter, useClass: CustomOverlayContainer}
],
bootstrap: [
AppComponent
]
})
export class AppModule {
constructor(library: FaIconLibrary, faConfig: FaConfig) {
library.addIconPacks(fas, far);
constructor(library: FaIconLibrary, faConfig: FaConfig, libraries: NbIconLibraries) {
library.addIconPacks(far, fas);
libraries.registerFontPack('solid', {packClass: 'fas', iconClassPrefix: 'fa'});
faConfig.defaultPrefix = 'fas';
libraries.setDefaultPack('solid');
}
}

View File

@ -6,22 +6,27 @@ import {HttpClient, HttpClientModule} from '@angular/common/http';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule, FlexModule} from '@angular/flex-layout';
import {MomentModule} from 'ngx-moment';
import {NotificationService} from '../shared/services/notification.service';
import {NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '../assets/@theme/theme.module';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NbMenuModule, NbOverlayContainerAdapter, NbSpinnerModule, NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
}
@NgModule({
declarations: [],
declarations: [
LoadingSpinnerComponent
],
imports: [
CommonModule,
NbToastrModule, // used for notification service
NbSpinnerModule,
FontAwesomeModule,
FlexLayoutModule,
ThemeModule.forRoot(),
NbMenuModule.forRoot(),
FlexModule,
HttpClientModule,
TranslateModule.forChild({
@ -34,9 +39,11 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
],
providers: [
HttpClient,
NotificationService
NotificationService,
NbOverlayContainerAdapter
],
exports: [
LoadingSpinnerComponent,
// modules
MomentModule
]

View File

@ -1,26 +1,50 @@
<div class="header" fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="2rem">
<div class="header" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="2rem">
<img *ngIf="currentTheme === 'corporate', else changeImage"
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
<ng-template #changeImage>
<img src="../../assets/images/favicons/corporate_favicon.ico" alt="logo light" class="header-icon" width="60rem" height="60rem">
<img src="../../assets/images/favicons/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem"
height="60rem">
</ng-template>
<div class="logo-container" fxLayoutAlign="center center">
<h1 >{{SECURITYC4PO_TITLE}} </h1>
<div class="logo-container">
<h1>{{SECURITYC4PO_TITLE}} </h1>
</div>
<div fxLayoutAlign="end" fxLayoutGap="4rem">
<div class="filler"></div>
<div fxLayoutGap="4rem">
<nb-actions size="medium">
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickSwitchTheme()">
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon" [icon]="fa.faMoon"
class="new-element-icon"></fa-icon>
<!--Info Action-->
<nb-action>
<fa-icon title="Info" [icon]="fa.faCircleInfo" (click)="onClickShowTutorial()" class="action-element-icon fa-2x">
</fa-icon>
</nb-action>
<!--OWASP Action-->
<nb-action>
<!-- Latest: https://owasp.org/www-project-web-security-testing-guide/latest/ -->
<!-- Stable: https://owasp.org/www-project-web-security-testing-guide/stable/ -->
<fa-icon title="OWASP Testing Guide"
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')"
[icon]="fa.faFileInvoice" class="action-element-icon fa-2x">
</fa-icon>
</nb-action>
<!--Theme Action-->
<nb-action>
<div (click)="onClickSwitchTheme()" class="action-element-icon">
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon"
title="Darktheme" [icon]="fa.faMoon" class="fa-2x">
</fa-icon>
<ng-template #changeIcon>
<fa-icon [icon]="fa.faSun" class="new-element-icon"></fa-icon>
<fa-icon title="Lighttheme" [icon]="fa.faSun" class="fa-2x"></fa-icon>
</ng-template>
</button>
</div>
</nb-action>
<!--User Action-->
<nb-action class="user-action">
<nb-user [nbContextMenu]="userMenu"
[picture]="FALLBACK_IMG"
name="{{user?.getValue()?.username}}"
title="Pentester">
</nb-user>
</nb-action>
</nb-actions>
</div>
</div>

View File

@ -1,6 +1,37 @@
@import '~@nebular/theme/styles/global/breakpoints';
@import '@nebular/theme/styles/global/breakpoints';
@import "../../assets/@theme/styles/_variables.scss";
.header {
flex: 1;
.filler {
flex-grow: 1;
}
.action-element-icon:hover {
cursor: pointer;
}
.logo-container {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.owasp-redirect-button {
margin-left: 0.5rem;
}
.user-action {
// width: 4rem;
z-index: 10;
// height: 3rem;
.user-action-accordion-header {
}
}
}
@mixin nb-overrides {
display: inline-flex;
justify-content: space-between;
@ -11,11 +42,6 @@
align-items: center;
width: auto;
.logo-container {
font-style: oblique;
color: #e74c3c;
}
nb-action {
height: auto;
display: flex;

View File

@ -3,16 +3,32 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
import {CommonModule} from '@angular/common';
import {FontAwesomeTestingModule} from '@fortawesome/angular-fontawesome/testing';
import {NbActionsModule} from '@nebular/theme';
import {NbActionsModule, NbMenuModule, NbMenuService, NbSelectModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {KeycloakService} from 'keycloak-angular';
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
import {User} from '@shared/models/user.model';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
userAccount: {
...new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US'),
id: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
isAuthenticated: true
};
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -22,7 +38,10 @@ describe('HeaderComponent', () => {
imports: [
CommonModule,
NbActionsModule,
NbSelectModule,
FontAwesomeTestingModule,
HttpClientTestingModule,
NbMenuModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -31,14 +50,24 @@ describe('HeaderComponent', () => {
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([])
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState])
],
providers: [
{provide: DialogService, useClass: DialogServiceMock},
NbMenuService,
KeycloakService
]
})
.compileComponents();
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,31 +1,141 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbThemeService} from '@nebular/theme';
import {map} from 'rxjs/operators';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
import {filter, map} from 'rxjs/operators';
import {GlobalTitlesVariables} from '@shared/config/global-variables';
import {TranslateService} from '@ngx-translate/core';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {KeycloakService} from 'keycloak-angular';
import {Store} from '@ngxs/store';
import {ResetSession} from '@shared/stores/session-state/session-state.actions';
import {UserService} from '@shared/services/user-service/user.service';
import {User} from '@shared/models/user.model';
import {BehaviorSubject} from 'rxjs';
import {Route} from '@shared/models/route.enum';
import {Router} from '@angular/router';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProfileSettingsComponent} from '@shared/modules/profile-settings/profile-settings.component';
import {TutorialDialogComponent} from '@shared/modules/tutorial-dialog/tutorial-dialog.component';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit, OnDestroy {
@UntilDestroy()
export class HeaderComponent implements OnInit {
// HTML only
readonly fa = FA;
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
// Menu only
readonly settingsIcon = 'gear';
readonly logoutIcon = 'right-from-bracket';
currentTheme = '';
constructor(private themeService: NbThemeService) { }
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
userMenu: NbMenuItem[] = [
{
title: 'settings',
icon: { icon: this.settingsIcon, pack: 'fas' }
},
{
title: 'logout',
icon: { icon: this.logoutIcon, pack: 'fas'}
}
];
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
constructor(
private store: Store,
private router: Router,
private themeService: NbThemeService,
private translateService: TranslateService,
private dialogService: DialogService,
private menuService: NbMenuService,
private userService: UserService,
protected keycloakService: KeycloakService) {
}
ngOnInit(): void {
// Handle theme selection
this.themeService.onThemeChange()
.pipe(
map(({ name }) => name),
map(({name}) => name),
untilDestroyed(this),
).subscribe(themeName => this.currentTheme = themeName);
// Load user profile
this.userService.loadUserProfile().pipe(
untilDestroyed(this)
).subscribe({
next: (user: User) => {
this.user.next(user);
},
error: err => {
console.error(err);
}
});
// Handle user profile menu selection
this.menuService.onItemClick()
.pipe(
untilDestroyed(this)
)
.subscribe(themeName => this.currentTheme = themeName);
.subscribe((menuBag) => {
// Makes sure that other menus without icon won't trigger
if (menuBag.item.icon) {
// tslint:disable-next-line:no-string-literal
if (menuBag.item.icon['icon'] === this.settingsIcon) {
this.dialogService.openCustomDialog(
ProfileSettingsComponent,
{
user: this.user.getValue(),
}
).onClose.pipe(
filter((confirm) => !!confirm),
untilDestroyed(this)
).subscribe({
next: () => {
console.info('New Settings confirmed');
}
});
}
// tslint:disable-next-line:no-string-literal
else if (menuBag.item.icon['icon'] === this.logoutIcon) {
this.onClickLogOut();
}
}
});
// Setup stream to translate menu item
this.translateService.stream('global.action.profile')
.pipe(
untilDestroyed(this)
).subscribe((text: string) => {
this.userMenu[0].title = text;
});
// Setup stream to translate menu item
this.translateService.stream('global.action.logout')
.pipe(
untilDestroyed(this)
).subscribe((text: string) => {
this.userMenu[1].title = text;
});
}
// HTML only
onClickGoToLink(url: string): void {
window.open(url, '_blank');
}
onClickShowTutorial(): void {
this.dialogService.openCustomDialog(
TutorialDialogComponent,
{}
).onClose.pipe(
filter((confirm) => !!confirm),
untilDestroyed(this)
).subscribe();
}
onClickSwitchTheme(): void {
@ -36,8 +146,19 @@ export class HeaderComponent implements OnInit, OnDestroy {
}
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
onClickLogOut(): void {
this.userService.logout().then(() => {
console.warn('logout success');
// Route user back to default page
this.router.navigate([Route.HOME]).then(() => {
// Reset User props from store
this.keycloakService.clearToken();
this.store.dispatch(new ResetSession());
}, err => {
console.error(err);
});
}, err => {
console.error(err);
});
}
}

View File

@ -1,9 +1,19 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from './header.component';
import {NbActionsModule, NbButtonModule, NbCardModule} from '@nebular/theme';
import {
NbActionsModule,
NbButtonModule,
NbCardModule,
NbContextMenuModule,
NbSelectModule,
NbUserModule
} from '@nebular/theme';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout';
import {TranslateModule} from '@ngx-translate/core';
import {ProfileSettingsModule} from '@shared/modules/profile-settings/profile-settings.module';
import {TutorialDialogModule} from '@shared/modules/tutorial-dialog/tutorial-dialog.module';
@NgModule({
declarations: [
@ -18,7 +28,15 @@ import {FlexLayoutModule} from '@angular/flex-layout';
FontAwesomeModule,
NbCardModule,
NbActionsModule,
FlexLayoutModule
FlexLayoutModule,
NbSelectModule,
TranslateModule,
NbUserModule,
NbContextMenuModule,
ProfileSettingsModule,
TutorialDialogModule
],
providers: [
]
})
export class HeaderModule {

View File

@ -1,4 +1,4 @@
@import '~@nebular/theme/styles/theming';
@import '@nebular/theme/styles/theming';
$login-width: 24em;
$input-width: 16rem;

View File

@ -21,8 +21,8 @@ import {ReactiveFormsModule} from '@angular/forms';
import {User} from '../../shared/models/user.model';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NotificationService} from '../../shared/services/notification.service';
import {NotificationServiceMock} from '../../shared/services/notification.service.mock';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {KeycloakService} from 'keycloak-angular';
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
@ -81,7 +81,6 @@ describe('LoginComponent', () => {
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
httpMock = TestBed.inject(HttpTestingController);

View File

@ -1,9 +1,9 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {Component, OnInit} from '@angular/core';
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {Store} from '@ngxs/store';
import {NotificationService, PopupType} from '../../shared/services/notification.service';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {User} from '../../shared/models/user.model';
import {throwError} from 'rxjs';
import {UpdateIsAuthenticated, UpdateUser} from '../../shared/stores/session-state/session-state.actions';
@ -12,16 +12,16 @@ import {HttpClient} from '@angular/common/http';
import {FieldStatus} from '../../shared/models/form-field-status.model';
import {KeycloakService} from 'keycloak-angular';
@UntilDestroy()
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
// ToDo: Exchange default Keycloak login with self made login
export class LoginComponent implements OnInit, OnDestroy {
export class LoginComponent implements OnInit {
readonly MIN_LENGTH: number = 2;
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
readonly NOVATEC_NAME = GlobalTitlesVariables.NOVATEC_NAME;
// ToDo: Remove after adding real authentication
private readonly user = new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US');
@ -29,7 +29,7 @@ export class LoginComponent implements OnInit, OnDestroy {
version: string;
// form control elements
loginFormGroup: FormGroup;
loginFormGroup: UntypedFormGroup;
loginUsernameCtrl: AbstractControl;
loginPasswordCtrl: AbstractControl;
@ -39,7 +39,7 @@ export class LoginComponent implements OnInit, OnDestroy {
formCtrlStatus = FieldStatus.BASIC;
constructor(private fb: FormBuilder,
constructor(private fb: UntypedFormBuilder,
private router: Router,
private store: Store,
private readonly httpClient: HttpClient,
@ -108,11 +108,6 @@ export class LoginComponent implements OnInit, OnDestroy {
return ctrlValue === '';
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
readAppVersion(): void {
this.httpClient.get<Version>('assets/version.json', {responseType: 'json'})
.subscribe((data: Version) => {

View File

@ -4,7 +4,7 @@ import {LoginComponent} from './login.component';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../common-app.module';
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 {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme';
import {ReactiveFormsModule} from '@angular/forms';

View File

@ -0,0 +1,2 @@
export {ObjectiveOverviewModule} from './objective-overview.module';
export {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module';

View File

@ -0,0 +1,5 @@
<div class="pentest-categories">
<nb-menu id="category-menu" class="menu-style" tag="menu" [items]="categories"></nb-menu>
</div>

View File

@ -0,0 +1,5 @@
@import '../../../assets/@theme/styles/themes';
.pentest-categories {
width: 20rem;
}

View File

@ -0,0 +1,58 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ObjectiveCategoriesComponent } from './objective-categories.component';
import {NbMenuModule, NbMenuService} from '@nebular/theme';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ThemeModule} from '@assets/@theme/theme.module';
import {RouterTestingModule} from '@angular/router/testing';
describe('ObjectiveCategoriesComponent', () => {
let component: ObjectiveCategoriesComponent;
let fixture: ComponentFixture<ObjectiveCategoriesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ObjectiveCategoriesComponent
],
imports: [
CommonModule,
BrowserAnimationsModule,
NbMenuModule.forRoot(),
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState]),
RouterTestingModule.withRoutes([]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
NbMenuService
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveCategoriesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,87 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {NbMenuItem, NbMenuService} from '@nebular/theme';
import {Store} from '@ngxs/store';
import {ChangeCategory} from '@shared/stores/project-state/project-state.actions';
import {Category} from '@shared/models/category.model';
import {TranslateService} from '@ngx-translate/core';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
@Component({
selector: 'app-objective-categories',
templateUrl: './objective-categories.component.html',
styleUrls: ['./objective-categories.component.scss']
})
@UntilDestroy()
export class ObjectiveCategoriesComponent implements OnInit {
categories: NbMenuItem[] = [];
selectedCategory: Category = 0;
constructor(private store: Store,
private menuService: NbMenuService,
private translateService: TranslateService) {
}
ngOnInit(): void {
this.initTranslation();
this.store.select(ProjectState.selectedCategory).pipe(
untilDestroyed(this)
).subscribe({
next: (categoryIndex) => {
if (categoryIndex) {
this.selectedCategory = categoryIndex;
this.categories[categoryIndex].selected = true;
} else {
// Set first item in list as selected
this.categories[0].selected = true;
}
},
error: error => {
console.error(error);
}
});
this.menuService.onItemClick()
.pipe(
untilDestroyed(this)
)
.subscribe((menuBag) => {
if (menuBag.tag === 'menu') {
this.selectedCategory = menuBag.item.data;
this.categories.forEach(category => {
category.selected = false;
});
if (this.selectedCategory >= 0) {
menuBag.item.selected = true;
this.store.dispatch(new ChangeCategory(this.selectedCategory));
}
}
});
}
private initTranslation(): void {
for (const cat in Category) {
if (isNaN(Number(cat))) {
// initialize category menu
this.translateService.get('categories.' + cat)
.pipe(
untilDestroyed(this)
)
.subscribe((text: string) => {
this.categories.push({title: text, data: Category[cat as keyof typeof Category]});
});
// set up continuous translation
this.translateService.stream('categories.' + cat)
.pipe(
untilDestroyed(this)
)
.subscribe((text: string) => {
this.categories.forEach(item => {
if (item.data === Category[cat as keyof typeof Category]) {
item.title = text;
}
});
});
}
}
}
}

View File

@ -0,0 +1,49 @@
<div class="pentest-header" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="space-between center">
<div class="back-button-container">
<button nbButton
shape="round"
title="{{ 'global.action.return' | translate }}"
(click)="onClickRouteBack()">
<fa-icon [icon]="fa.faLongArrowAltLeft"
class="back-element-icon fa-lg"></fa-icon>
</button>
</div>
<div class="header-info" fxLayout="row" fxLayoutGap="4rem" fxLayoutAlign="space-between center">
<app-report-state-tag class="state-tag"
[currentReportState]="selectedProject$.getValue()?.state"></app-report-state-tag>
<h4 class="project-title">{{selectedProject$.getValue().title}}</h4>
<app-version-tag [version]="selectedProject$.getValue().version"></app-version-tag>
</div>
<div class="button-container">
<!--Actions for normal view-->
<nb-actions size="medium" fxHide.lt-lg>
<nb-action>
<button nbButton
status="primary"
shape="round"
(click)="onClickEditPentestProject()">
<fa-icon [icon]="fa.faEdit"
class="element-icon fa-lg"></fa-icon>
</button>
</nb-action>
<nb-action>
<button nbButton hero
status="info"
shape="round"
(click)="onClickGeneratePentestReport()">
<fa-icon [icon]="fa.faFileAlt"
class="element-icon fa-lg"></fa-icon>
<span class="element-text">{{ 'global.action.report' | translate }}</span>
</button>
</nb-action>
</nb-actions>
<!--Actions for mobile devices-->
<nb-actions size="medium" fxHide fxShow.lt-lg>
<nb-action>
<nb-user [nbContextMenu]="objectiveActionItems" shape="rectangle" [picture]="BARS_IMG" name="" [onlyPicture]></nb-user>
</nb-action>
</nb-actions>
</div>
</div>

View File

@ -0,0 +1,37 @@
@import '../../../assets/@theme/styles/_text-overflow.scss';
.pentest-header {
width: 100vw;
.back-button-container {
.back-element-icon {
}
}
.header-info {
position: absolute;
margin-left: 10rem;
margin-right: 10rem;
text-align: center;
.state-tag {
}
.project-title {
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 2rem, $lines-to-show: 1, $max-width: 36rem);
}
}
.button-container {
position: absolute;
right: 2rem;
.element-icon {
}
.element-text {
padding-left: 0.5rem;
font-size: 0.85rem;
}
}
}

View File

@ -0,0 +1,111 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ObjectiveHeaderComponent} from './objective-header.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbActionsModule, NbIconModule, NbMenuService} from '@nebular/theme';
import {ProjectService} from '@shared/services/api/project.service';
import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {Category} from '@shared/models/category.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';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('ObjectiveHeaderComponent', () => {
let component: ObjectiveHeaderComponent;
let fixture: ComponentFixture<ObjectiveHeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ObjectiveHeaderComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
ThemeModule.forRoot(),
FontAwesomeModule,
NbIconModule,
NbActionsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
NbMenuService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: ExportReportDialogService, useClass: ExportReportDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveHeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,176 @@
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {Route} from '@shared/models/route.enum';
import {Store} from '@ngxs/store';
import {Router} from '@angular/router';
import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {BehaviorSubject} from 'rxjs';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
import {filter, mergeMap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {ProjectService} from '@shared/services/api/project.service';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
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';
import {NbMenuItem} from '@nebular/theme/components/menu/menu.service';
import {NbMenuService} from '@nebular/theme';
import {TranslateService} from '@ngx-translate/core';
@UntilDestroy()
@Component({
selector: 'app-objective-header',
templateUrl: './objective-header.component.html',
styleUrls: ['./objective-header.component.scss']
})
export class ObjectiveHeaderComponent implements OnInit {
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
// Menu only
readonly editIcon = 'edit';
readonly fileExportIcon = 'file-export';
// Mobile menu properties
objectiveActionItems: NbMenuItem[] = [
{
title: 'global.action.edit',
icon: { icon: this.editIcon, pack: 'fas' }
},
{
title: 'global.action.report',
icon: { icon: this.fileExportIcon, pack: 'fas' }
},
];
// HTML only
readonly fa = FA;
readonly BARS_IMG = 'assets/images/icons/bars.svg';
readonly ELLIPSIS_IMG = 'assets/images/icons/ellipsis.svg';
constructor(private store: Store,
private readonly notificationService: NotificationService,
private dialogService: DialogService,
private projectDialogService: ProjectDialogService,
private projectService: ProjectService,
private exportReportDialogService: ExportReportDialogService,
private readonly router: Router,
private translateService: TranslateService,
private menuService: NbMenuService
) {
}
ngOnInit(): void {
this.store.select(ProjectState.project).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedProject: Project) => {
if (selectedProject) {
this.selectedProject$.next(selectedProject);
} else {
this.router.navigate([Route.PROJECT_OVERVIEW]);
}
},
error: err => {
console.error(err);
}
});
// Handle user profile menu action selection
this.menuService.onItemClick()
.pipe(
untilDestroyed(this)
)
.subscribe((menuBag) => {
// Makes sure that other menus without icon won't trigger
if (menuBag.item.icon) {
// tslint:disable-next-line:no-string-literal
if (menuBag.item.icon['icon'] === this.editIcon) {
this.onClickEditPentestProject();
}
// tslint:disable-next-line:no-string-literal
else if (menuBag.item.icon['icon'] === this.fileExportIcon) {
this.onClickGeneratePentestReport();
}
}
});
// Setup stream to translate menu action item
this.translateService.stream('global.action.edit')
.pipe(
untilDestroyed(this)
).subscribe((text: string) => {
this.objectiveActionItems[0].title = text;
});
// Setup stream to translate menu action item
this.translateService.stream('global.action.report')
.pipe(
untilDestroyed(this)
).subscribe((text: string) => {
this.objectiveActionItems[1].title = text;
});
}
onClickRouteBack(): void {
this.router.navigate([Route.PROJECT_OVERVIEW])
.then(
() => this.store.reset({
...this.store.snapshot(),
[PROJECT_STATE_NAME]: undefined
})
).finally();
}
onClickEditPentestProject(): void {
this.projectDialogService.openProjectDialog(
ProjectDialogComponent,
this.selectedProject$.getValue(),
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
}
).pipe(
untilDestroyed(this)
).subscribe({
next: (project) => {
if (project) {
this.store.dispatch(new InitProjectState(
project,
[],
[]
)).pipe(
untilDestroyed(this)
).subscribe();
}
}
});
}
onClickGeneratePentestReport(): void {
this.exportReportDialogService.openExportReportDialog(
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

@ -0,0 +1,12 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ObjectiveOverviewRoutingModule {
}

View File

@ -0,0 +1,77 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ObjectiveHeaderComponent} from './objective-header/objective-header.component';
import {ObjectiveCategoriesComponent} from './objective-categories/objective-categories.component';
import {ObjectiveTableComponent} from './objective-table/objective-table.component';
import {
NbCardModule,
NbLayoutModule,
NbTreeGridModule,
NbMenuModule,
NbListModule,
NbButtonModule,
NbTooltipModule,
NbActionsModule, NbUserModule, NbContextMenuModule, NbSortDirective
} from '@nebular/theme';
import {TranslateModule} from '@ngx-translate/core';
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
import {FindigWidgetModule} from '@shared/widgets/findig-widget/findig-widget.module';
import {RouterModule} from '@angular/router';
import {FormsModule} from '@angular/forms';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout';
import {CommonAppModule} from '../common-app.module';
import {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module';
import {ExportReportDialogModule} from '@shared/modules/export-report-dialog/export-report-dialog.module';
import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module';
import {CommentWidgetModule} from '@shared/widgets/comment-widget/comment-widget.module';
import {ReportStateTagModule} from '@shared/widgets/report-state-tag/report-state-tag.module';
import {VersionTagModule} from '@shared/widgets/version-tag/version-tag.module';
@NgModule({
declarations: [
ObjectiveHeaderComponent,
ObjectiveCategoriesComponent,
ObjectiveTableComponent
],
imports: [
CommonModule,
CommonAppModule,
NbLayoutModule,
NbCardModule,
NbButtonModule,
// nbTooltip crashes app right now if used in component,
// workaround: use title in html for now
NbTooltipModule,
NbTreeGridModule,
TranslateModule,
StatusTagModule,
RouterModule,
FormsModule,
NbListModule,
FontAwesomeModule,
FlexLayoutModule,
NbActionsModule,
ExportReportDialogModule,
ProjectDialogModule,
ObjectiveOverviewRoutingModule,
// Table Widgets
FindigWidgetModule,
CommentWidgetModule,
NbMenuModule,
ReportStateTagModule,
VersionTagModule,
NbUserModule,
NbContextMenuModule
],
exports: [
ObjectiveHeaderComponent,
ObjectiveCategoriesComponent,
ObjectiveTableComponent
],
providers: [
NbSortDirective
]
})
export class ObjectiveOverviewModule {
}

View File

@ -0,0 +1,92 @@
<div class="pentest-table">
<table [nbTreeGrid]="dataSource">
<!--ToDo: Add the click event to every td manually except the actions column actions-->
<tr nbTreeGridHeaderRow *nbTreeGridHeaderRowDef="columns"></tr>
<tr nbTreeGridRow *nbTreeGridRowDef="let pentest; columns: columns"
class="pentest-cell"
[ngClass]="{'disabled-objective' : !pentest.data['enabled']}">
</tr>
<!-- Test ID -->
<ng-container [nbTreeGridColumnDef]="columns[0]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'pentest.testId' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let pentest" (click)="onClickRouteToObjectivePentest(pentest.data)">
<!-- Opens sub categories if row needs to be extendend -->
<nb-tree-grid-row-toggle
[expanded]="pentest.expanded"
*ngIf="pentest.data?.childEntries?.length > 0">
</nb-tree-grid-row-toggle>
<!---->
{{pentest.data['refNumber'] || '-'}}
</td>
</ng-container>
<!-- Title -->
<ng-container [nbTreeGridColumnDef]="columns[1]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'pentest.title' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let pentest" (click)="onClickRouteToObjectivePentest(pentest.data)">
{{ getTitle(pentest.data['refNumber']) | translate }}
</td>
</ng-container>
<!-- Status -->
<ng-container [nbTreeGridColumnDef]="columns[2]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'pentest.status' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let pentest" (click)="onClickRouteToObjectivePentest(pentest.data)">
<app-status-tag [currentStatus]="pentest.data['status']"></app-status-tag>
</td>
</ng-container>
<!-- Findings -->
<ng-container [nbTreeGridColumnDef]="columns[3]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'pentest.findings&comments' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let pentest" (click)="onClickRouteToObjectivePentest(pentest.data)">
<div fxLayout="row" fxLayoutGap="0.5rem" fxLayoutAlign="center center">
<app-findig-widget [numberOfFindings]="pentest.data['findingIds'] ? pentest.data['findingIds'].length : 0"></app-findig-widget>
<span> / </span>
<app-comment-widget [numberOfComments]="pentest.data['commentIds'] ? pentest.data['commentIds'].length : 0"></app-comment-widget>
</div>
</td>
</ng-container>
<!-- Actions -->
<ng-container [nbTreeGridColumnDef]="columns[4]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-actions">
{{'global.actions' | translate}}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let pentest" class="cell-actions">
<div fxLayoutAlign="center center">
<ng-container *ngIf="pentest.data['enabled'] === true; else renderDisablePentestButton">
<button
nbButton
status="danger"
size="small"
shape="round"
title="{{ 'global.action.disable' | translate }}"
[disabled]="!pentest.data['id']"
(click)="onClickDisableOrEnableObjective(pentest)">
<fa-icon [icon]="fa.faBan"></fa-icon>
</button>
</ng-container>
<ng-template #renderDisablePentestButton>
<button
nbButton
status="control"
size="small"
shape="round"
title="{{ 'global.action.enable' | translate }}"
[disabled]="!pentest.data['id']"
(click)="onClickDisableOrEnableObjective(pentest)">
<fa-icon [icon]="fa.faCheck"></fa-icon>
</button>
</ng-template>
</div>
</td>
</ng-container>
</table>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,31 @@
@import '../../../assets/@theme/styles/themes';
.pentest-table {
// width: calc(78vw - 18%);
// width: 100%;
// width: calc(100% - 20rem);
margin-right: 2rem;
padding-right: 2rem;
.pentest-cell {
// Add style here
}
.pentest-cell:hover {
cursor: pointer;
background-color: nb-theme(color-basic-transparent-focus);
}
.disabled-objective {
background-color: nb-theme(color-control-transparent-disabled);
}
.disabled-objective:hover {
cursor: not-allowed;
}
.cell-actions {
width: max-content;
max-width: 180px;
}
}

View File

@ -0,0 +1,66 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ObjectiveTableComponent} from './objective-table.component';
import {NbCardModule, NbTreeGridModule} from '@nebular/theme';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ThemeModule} from '@assets/@theme/theme.module';
import {RouterTestingModule} from '@angular/router/testing';
import {StatusTagComponent} from '@shared/widgets/status-tag/status-tag.component';
import {FindigWidgetComponent} from '@shared/widgets/findig-widget/findig-widget.component';
import {MockComponent} from 'ng-mocks';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
describe('ObjectiveTableComponent', () => {
let component: ObjectiveTableComponent;
let fixture: ComponentFixture<ObjectiveTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ObjectiveTableComponent,
MockComponent(StatusTagComponent),
MockComponent(FindigWidgetComponent)
],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
NbCardModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useClass: NotificationServiceMock}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,192 @@
import {Component, OnInit} from '@angular/core';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {ObjectiveEntry, Pentest, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/api/pentest.service';
import {Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {catchError, filter, switchMap, tap} from 'rxjs/operators';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function';
import {Router} from '@angular/router';
import {ChangePentest} from '@shared/stores/project-state/project-state.actions';
import {Route} from '@shared/models/route.enum';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {Project} from '@shared/models/project.model';
import {sortDescending} from '@shared/functions/sort-names.function';
@UntilDestroy()
@Component({
selector: 'app-objective-table',
templateUrl: './objective-table.component.html',
styleUrls: ['./objective-table.component.scss']
})
export class ObjectiveTableComponent implements OnInit {
// HTML only
readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
columns: Array<ObjectiveColumns> = [
ObjectiveColumns.TEST_ID,
ObjectiveColumns.TITLE,
ObjectiveColumns.STATUS,
ObjectiveColumns.FINDINGS_AND_COMMENTS,
ObjectiveColumns.ACTIONS
];
dataSource: NbTreeGridDataSource<ObjectiveEntry>;
private data: ObjectiveEntry[] = [];
private pentests$: BehaviorSubject<Pentest[]> = new BehaviorSubject<Pentest[]>([]);
// Needed for pentest enabling and disabling
selectedProjectId$: BehaviorSubject<string> = new BehaviorSubject<string>('');
getters: NbGetters<ObjectiveEntry, ObjectiveEntry> = {
dataGetter: (node: ObjectiveEntry) => node,
childrenGetter: (node: ObjectiveEntry) => node.childEntries || undefined,
expandedGetter: (node: ObjectiveEntry) => !!node.expanded
};
constructor(
private store: Store,
private pentestService: PentestService,
private dialogService: DialogService,
private notificationService: NotificationService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<ObjectiveEntry>,
private router: Router
) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
}
ngOnInit(): void {
this.store.selectOnce(ProjectState.project).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedProject: Project) => {
this.selectedProjectId$.next(selectedProject.id);
},
error: err => {
console.error(err);
}
});
this.loadPentestData();
}
loadPentestData(): void {
this.store.select(ProjectState.selectedCategory).pipe(
switchMap(category => this.pentestService.loadPentests(category)),
tap(() => this.loading$.next(true)),
catchError(_ => of(null)),
untilDestroyed(this)
).subscribe({
next: (pentests: Pentest[]) => {
// Sort data without before adding as table data source
const sortedPentests = pentests.sort((a: Pentest, b: Pentest) =>
sortDescending(a.refNumber.toLowerCase(), b.refNumber.toLowerCase())
);
this.pentests$.next(sortedPentests);
this.data = transformPentestsToObjectiveEntries(sortedPentests);
this.dataSource.setData(this.data, this.getters);
this.loading$.next(false);
},
error: error => {
this.loading$.next(false);
console.error(error);
}
});
}
onClickRouteToObjectivePentest(selectedPentest: Pentest): void {
if (selectedPentest.enabled) {
this.router.navigate([Route.PENTEST_OBJECTIVE])
.then(
() => this.store.reset({
...this.store.snapshot(),
})
).finally();
// Change Pentest State
const statePentest: Pentest = this.pentests$.getValue().find(pentest => pentest.refNumber === selectedPentest.refNumber);
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));
}
}
}
onClickDisableOrEnableObjective(pentest): void {
if (pentest.data.enabled) {
const message = {
title: 'pentest.disable.title',
key: 'pentest.disable.key',
data: {name: pentest.data.refNumber},
};
this.dialogService.openConfirmDialog(
message
).onClose.pipe(
filter((confirm) => !!confirm),
untilDestroyed(this)
).subscribe({
next: () => {
this.pentestService.disableObjective(this.selectedProjectId$.getValue(), pentest.data.id).pipe(
untilDestroyed(this)
).subscribe({
next: () => {
this.loadPentestData();
this.notificationService.showPopup('pentest.popup.disable.success', PopupType.SUCCESS);
},
error: (err) => {
this.notificationService.showPopup('pentest.popup.disable.failed', PopupType.FAILURE);
console.error(err);
}
});
}
});
} else {
this.pentestService.enableObjective(this.selectedProjectId$.getValue(), pentest.data.id).pipe(
untilDestroyed(this)
).subscribe({
next: () => {
this.loadPentestData();
this.notificationService.showPopup('pentest.popup.enable.success', PopupType.SUCCESS);
},
error: (err) => {
this.notificationService.showPopup('pentest.popup.enable.failed', PopupType.FAILURE);
console.error(err);
}
});
}
}
// HTML only
getTitle(refNumber: string): string {
return getTitleKeyForRefNumber(refNumber);
}
// HTML only
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
}
enum ObjectiveColumns {
TEST_ID = 'testId',
TITLE = 'title',
STATUS = 'status',
FINDINGS_AND_COMMENTS = 'findings&comments',
ACTIONS = 'actions'
}

View File

@ -1,2 +0,0 @@
export {PentestOverviewModule} from './pentest-overview.module';
export {PentestOverviewRoutingModule} from './pentest-overview-routing.module';

View File

@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PentestCategoriesComponent } from './pentest-categories.component';
describe('PentestCategoriesComponent', () => {
let component: PentestCategoriesComponent;
let fixture: ComponentFixture<PentestCategoriesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PentestCategoriesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestCategoriesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pentest-categories',
templateUrl: './pentest-categories.component.html',
styleUrls: ['./pentest-categories.component.scss']
})
export class PentestCategoriesComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,3 +0,0 @@
<div>
<p>header for "{{selectedProjectTitle}}" works!</p>
</div>

View File

@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PentestHeaderComponent } from './pentest-header.component';
describe('PentestHeaderComponent', () => {
let component: PentestHeaderComponent;
let fixture: ComponentFixture<PentestHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PentestHeaderComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,17 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pentest-header',
templateUrl: './pentest-header.component.html',
styleUrls: ['./pentest-header.component.scss']
})
export class PentestHeaderComponent implements OnInit {
selectedProjectTitle: string = history?.state?.selectedProject ? history?.state?.selectedProject.title : '';
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,13 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule, Routes} from '@angular/router';
import {ProjectComponent} from '../project-overview/project/project.component';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PentestOverviewRoutingModule {
}

View File

@ -1,25 +0,0 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {PentestHeaderComponent} from './pentest-header/pentest-header.component';
import {PentestCategoriesComponent} from './pentest-categories/pentest-categories.component';
import {PentestTableComponent} from './pentest-table/pentest-table.component';
import {NbLayoutModule} from '@nebular/theme';
@NgModule({
declarations: [
PentestHeaderComponent,
PentestCategoriesComponent,
PentestTableComponent
],
exports: [
PentestHeaderComponent,
PentestCategoriesComponent,
PentestTableComponent
],
imports: [
CommonModule,
NbLayoutModule
]
})
export class PentestOverviewModule {
}

View File

@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pentest-table',
templateUrl: './pentest-table.component.html',
styleUrls: ['./pentest-table.component.scss']
})
export class PentestTableComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,2 @@
export {PentestModule} from './pentest.module';
export {PentestRoutingModule} from './pentest-routing.module';

View File

@ -0,0 +1,76 @@
<div class="comment-table">
<table [nbTreeGrid]="dataSource">
<tr nbTreeGridHeaderRow *nbTreeGridHeaderRowDef="columns"></tr>
<tr nbTreeGridRow *nbTreeGridRowDef="let comment; columns: columns"
class="comment-cell"
fragment="{{comment.data['commentId']}}">
</tr>
<!-- Title -->
<ng-container [nbTreeGridColumnDef]="columns[0]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'comment.title' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let comment">
<span *ngIf=" comment.data['title'].length < 200; else cutTitle">
{{ comment.data['title'] }}
</span>
<ng-template #cutTitle>
{{ comment.data['title'].slice(0, 200) + '...' }}
</ng-template>
</td>
</ng-container>
<!-- Description -->
<ng-container [nbTreeGridColumnDef]="columns[1]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'comment.description' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let comment">
<span *ngIf=" comment.data['description'].length < 200; else cutDescription">
{{ comment.data['description'] }}
</span>
<ng-template #cutDescription>
{{ comment.data['description'].slice(0, 200) + '...' }}
</ng-template>
</td>
</ng-container>
<!-- Actions -->
<ng-container [nbTreeGridColumnDef]="columns[2]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-actions">
<button nbButton hero
status="info"
size="small"
shape="round"
class="add-comment-button"
[disabled]="pentestInfo$.getValue().status !== inProgressStatus"
(click)="onClickAddComment()">
<fa-icon [icon]="fa.faPlus" class="new-comment-icon"></fa-icon>
{{'comment.add' | translate}}
</button>
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let comment" class="cell-actions">
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="1rem">
<button nbButton
status="primary"
size="small"
(click)="onClickEditComment(comment)">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<button nbButton
status="danger"
size="small"
(click)="onClickDeleteComment(comment)">
<fa-icon [icon]="fa.faTrash"></fa-icon>
</button>
</div>
</td>
</ng-container>
</table>
</div>
<div *ngIf="data.length === 0 && loading$.getValue() === false" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'comment.no.comments' | translate}}
</p>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,49 @@
@import '../../../../assets/@theme/styles/themes';
@import '../../../../assets/@theme/styles/_text-overflow.scss';
.comment-table {
margin-right: 2rem;
padding-right: 2rem;
.comment-cell {
// Add style here
height: 4.5rem !important;
// max-height: 4.5rem !important;
overflow: hidden;
}
.comment-cell:hover {
// cursor: default;
background-color: nb-theme(color-basic-transparent-focus);
}
.cell {
height: 4.5rem !important;
max-height: 4.5rem !important;
}
.related-finding-cell {
height: 4.5rem !important;
max-height: 4.5rem !important;
// cursor: pointer;
font-family: Courier, serif;
color: nb-theme(color-danger-default);
}
.cell-actions {
width: max-content;
max-width: 200px;
.add-comment-button {
.new-comment-icon {
padding-right: 0.5rem;
}
}
}
}
.error-text {
padding-top: 0.5rem;
font-size: 1.25rem;
font-weight: bold;
}

View File

@ -0,0 +1,109 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestCommentsComponent} from './pentest-comments.component';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NgxsModule, Store} from '@ngxs/store';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('PentestCommentsComponent', () => {
let component: PentestCommentsComponent;
let fixture: ComponentFixture<PentestCommentsComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestCommentsComponent,
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
NbButtonModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestCommentsComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,235 @@
import {Component, OnInit} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, tap} from 'rxjs/operators';
import {
Comment,
CommentEntry,
transformCommentsToObjectiveEntries
} from '@shared/models/comment.model';
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';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentService} from '@shared/services/api/comment.service';
import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Finding} from '@shared/models/finding.model';
import {FindingService} from '@shared/services/api/finding.service';
@UntilDestroy()
@Component({
selector: 'app-pentest-comments',
templateUrl: './pentest-comments.component.html',
styleUrls: ['./pentest-comments.component.scss']
})
export class PentestCommentsComponent implements OnInit {
// HTML only
readonly fa = FA;
// HTML only for button enabling
inProgressStatus: PentestStatus = PentestStatus.IN_PROGRESS;
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
// comments$: BehaviorSubject<Comment[]> = new BehaviorSubject<Comment[]>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
columns: Array<CommentColumns> = [
CommentColumns.TITLE, CommentColumns.DESCRIPTION, CommentColumns.ACTIONS
];
dataSource: NbTreeGridDataSource<CommentEntry>;
data: CommentEntry[] = [];
getters: NbGetters<CommentEntry, CommentEntry> = {
dataGetter: (node: CommentEntry) => node,
childrenGetter: (node: CommentEntry) => node.childEntries || undefined,
expandedGetter: (node: CommentEntry) => !!node.expanded,
};
constructor(private readonly commentService: CommentService,
private readonly findingService: FindingService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
private notificationService: NotificationService,
private dialogService: DialogService,
private commentDialogService: CommentDialogService,
private store: Store) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
}
ngOnInit(): void {
this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
this.pentestInfo$.next(selectedPentest);
this.loadCommentsData();
this.requestFindingsData(selectedPentest.id);
},
error: err => {
console.error(err);
}
});
}
loadCommentsData(): void {
this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe(
untilDestroyed(this),
/*filter(isNotNullOrUndefined),*/
tap(() => this.loading$.next(true))
)
.subscribe({
next: (comments: Comment[]) => {
if (comments) {
this.data = transformCommentsToObjectiveEntries(comments);
} else {
this.data = [];
}
this.dataSource.setData(this.data, this.getters);
this.loading$.next(false);
},
error: err => {
console.error(err);
// ToDo: Implement again after proper lazy loading and routing
// this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE);
this.loading$.next(false);
}
});
}
onClickAddComment(): void {
this.commentDialogService.openCommentDialog(
CommentDialogComponent,
this.pentestInfo$.getValue().findingIds,
null,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
},
this.pentestInfo$.getValue()
).pipe(
untilDestroyed(this)
).subscribe({
next: (newComment: Comment) => {
this.loadCommentsData();
}
});
}
onClickEditComment(commentEntry): void {
this.commentService.getCommentById(commentEntry.data.commentId).pipe(
filter(isNotNullOrUndefined),
untilDestroyed(this)
).subscribe({
next: (existingComment: Comment) => {
if (existingComment) {
this.commentDialogService.openCommentDialog(
CommentDialogComponent,
this.pentestInfo$.getValue().findingIds,
existingComment,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
},
this.pentestInfo$.getValue()
).pipe(
untilDestroyed(this)
).subscribe({
next: (updatedComment: Comment) => {
this.loadCommentsData();
}
});
} else {
this.notificationService.showPopup('comment.popup.not.available', PopupType.INFO);
}
},
error: err => {
console.error(err);
}
});
}
requestFindingsData(pentestId: string): void {
this.findingService.getFindingsByPentestId(pentestId).pipe(
untilDestroyed(this)
).subscribe({
next: (findings: Finding[]) => {
// findings.forEach(finding => this.objectiveFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
},
error: err => {
console.error(err);
}
});
}
onClickDeleteComment(commentEntry): void {
const message = {
title: 'comment.delete.title',
key: 'comment.delete.key',
data: {name: commentEntry.data.title},
};
this.dialogService.openConfirmDialog(
message
).onClose.pipe(
untilDestroyed(this)
).subscribe({
next: () => {
this.deleteComment(commentEntry);
}
});
}
// HTML only
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
private deleteComment(commentEntry): void {
this.commentService.deleteCommentByPentestAndCommentId(
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
commentEntry.data.commentId)
.pipe(
untilDestroyed(this)
).subscribe({
next: (deletedComment: any) => {
this.store.dispatch(new UpdatePentestComments(deletedComment.id));
this.loadCommentsData();
this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS);
}, error: error => {
console.error(error);
this.onRequestFailed(commentEntry);
this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE);
}
});
}
private onRequestFailed(retryParameter: any): void {
this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose
.pipe(
untilDestroyed(this)
)
.subscribe((ref) => {
if (ref.retry) {
this.deleteComment(retryParameter);
}
});
}
}
enum CommentColumns {
COMMENT_ID = 'commentId',
TITLE = 'title',
DESCRIPTION = 'description',
RELATED_FINDINGS = 'relatedFindings',
ACTIONS = 'actions'
}

View File

@ -0,0 +1,22 @@
<div class="pentest-content">
<div class="content">
<nb-tabset>
<nb-tab class="pentest-tabset" tabTitle="{{ 'global.action.info' | translate }}">
<app-pentest-info></app-pentest-info>
</nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.findings' | translate }}"
badgeText="{{currentNumberOfFindings$.getValue()}}" badgeStatus="danger">
<app-pentest-findings></app-pentest-findings>
</nb-tab>
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}"
badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="control">
<app-pentest-comments></app-pentest-comments>
</nb-tab>
</nb-tabset>
</div>
<div fxLayoutAlign="end end" class="content-footer">
<!--ToDo: Use to put element in bottom-right corner -->
</div>
</div>

View File

@ -0,0 +1,20 @@
.pentest-content {
width: 100%;
height: 100%;
.content {
height: 95%;
overflow: auto !important;
// overflow: hidden !important;
// ToDo: Fixes tab header but also disables scrolling for content
/*nb-tab {
position: fixed;
}*/
.content-footer {
height: 5%;
margin: 1rem 6rem 1rem 0;
}
}
}

View File

@ -0,0 +1,93 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestContentComponent} from './pentest-content.component';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: [],
commentIds: []
},
};
describe('PentestContentComponent', () => {
let component: PentestContentComponent;
let fixture: ComponentFixture<PentestContentComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestContentComponent
],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestContentComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,54 @@
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {BehaviorSubject} from 'rxjs';
import {Store} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Pentest} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/api/pentest.service';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {Router} from '@angular/router';
import {Route} from '@shared/models/route.enum';
@UntilDestroy()
@Component({
selector: 'app-pentest-content',
templateUrl: './pentest-content.component.html',
styleUrls: ['./pentest-content.component.scss']
})
export class PentestContentComponent implements OnInit {
// HTML only
readonly fa = FA;
pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
currentNumberOfFindings$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
currentNumberOfComments$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
constructor(
private readonly pentestService: PentestService,
private notificationService: NotificationService,
private router: Router,
private store: Store) {
}
ngOnInit(): void {
this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
if (selectedPentest) {
this.pentest$.next(selectedPentest);
const findings = selectedPentest.findingIds ? selectedPentest.findingIds.length : 0;
this.currentNumberOfFindings$.next(findings);
const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0;
this.currentNumberOfComments$.next(comments);
} else {
this.router.navigate([Route.PROJECT_OVERVIEW]);
}
},
error: err => {
console.error(err);
}
});
}
}

View File

@ -0,0 +1,101 @@
<div class="finding-table">
<table [nbTreeGrid]="dataSource">
<tr nbTreeGridHeaderRow *nbTreeGridHeaderRowDef="columns"></tr>
<tr nbTreeGridRow *nbTreeGridRowDef="let finding; columns: columns"
class="finding-cell"
fragment="{{finding.data['findingId']}}">
</tr>
<!-- Title -->
<ng-container [nbTreeGridColumnDef]="columns[0]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.title' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
<span *ngIf=" finding.data['title'].length < 200; else cutTitle">
{{ finding.data['title'] }}
</span>
<ng-template #cutTitle>
{{ finding.data['title'].slice(0, 200) + '...' }}
</ng-template>
</td>
</ng-container>
<!-- Severity -->
<ng-container [nbTreeGridColumnDef]="columns[1]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-severity">
{{ 'finding.severity' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding" class="cell-severity border-style">
<div fxLayoutAlign="center center">
<app-severity-tag [currentSeverity]="finding.data['severity']"></app-severity-tag>
</div>
</td>
</ng-container>
<!-- Description -->
<ng-container [nbTreeGridColumnDef]="columns[2]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.description' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
<span *ngIf=" finding.data['description'].length < 200; else cutDescription">
{{ finding.data['description'] }}
</span>
<ng-template #cutDescription>
{{ finding.data['description'].slice(0, 200) + '...' }}
</ng-template>
</td>
</ng-container>
<!-- Impact -->
<ng-container [nbTreeGridColumnDef]="columns[3]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
{{ 'finding.impact' | translate }}
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding">
<span *ngIf=" finding.data['impact'].length < 200; else cutImpact">
{{ finding.data['impact'] }}
</span>
<ng-template #cutImpact>
{{ finding.data['impact'].slice(0, 200) + '...' }}
</ng-template>
</td>
</ng-container>
<!-- Actions -->
<ng-container [nbTreeGridColumnDef]="columns[4]">
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef class="cell-actions">
<button nbButton hero
status="info"
size="small"
shape="round"
class="add-finding-button"
[disabled]="pentestInfo$.getValue().status !== inProgressStatus"
(click)="onClickAddFinding()">
<fa-icon [icon]="fa.faPlus" class="new-finding-icon"></fa-icon>
{{'finding.add' | translate}}
</button>
</th>
<td nbTreeGridCell *nbTreeGridCellDef="let finding" class="cell-actions">
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="1rem">
<button nbButton
status="primary"
size="small"
(click)="onClickEditFinding(finding)">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<button nbButton
status="danger"
size="small"
(click)="onClickDeleteFinding(finding)">
<fa-icon [icon]="fa.faTrash"></fa-icon>
</button>
</div>
</td>
</ng-container>
</table>
</div>
<div *ngIf="data.length === 0 && loading$.getValue() === false" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'finding.no.findings' | translate}}
</p>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -0,0 +1,54 @@
@import '../../../../assets/@theme/styles/themes';
@import '../../../../assets/@theme/styles/_text-overflow.scss';
.finding-table {
margin-right: 2rem;
padding-right: 2rem;
.finding-cell {
// Add style here
height: 4.5rem !important;
// max-height: 4.5rem !important;
overflow: hidden;
}
.finding-cell:hover {
// cursor: default;
background-color: nb-theme(color-basic-transparent-focus);
}
.cell-severity {
//width: 125px;
// max-width: 125px;
// border-style: none;
// ToDo: Fix size issue on lower screen resolution
// height: 4.5rem !important;
}
.cell {
height: 4.5rem !important;
max-height: 4.5rem !important;
}
.border-style {
border-top-style: none;
border-left-style: none;
}
.cell-actions {
width: max-content;
max-width: 180px;
.add-finding-button {
.new-finding-icon {
padding-right: 0.5rem;
}
}
}
}
.error-text {
padding-top: 0.5rem;
font-size: 1.25rem;
font-weight: bold;
}

View File

@ -0,0 +1,110 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestFindingsComponent} from './pentest-findings.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {CommonModule} from '@angular/common';
import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentIds: []
},
};
describe('PentestFindingsComponent', () => {
let component: PentestFindingsComponent;
let fixture: ComponentFixture<PentestFindingsComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestFindingsComponent,
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
NbButtonModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestFindingsComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
component.pentestInfo$.next(DESIRED_PROJECT_STATE_SESSION.selectedPentest);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,217 @@
import {Component, OnInit} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import { filter, tap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {
Finding,
FindingEntry,
transformFindingsToObjectiveEntries,
} from '@shared/models/finding.model';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
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';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {FindingService} from '@shared/services/api/finding.service';
@UntilDestroy()
@Component({
selector: 'app-pentest-findings',
templateUrl: './pentest-findings.component.html',
styleUrls: ['./pentest-findings.component.scss']
})
export class PentestFindingsComponent implements OnInit {
constructor(private findingService: FindingService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
private readonly notificationService: NotificationService,
private dialogService: DialogService,
private findingDialogService: FindingDialogService,
private store: Store) {
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
}
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
// HTML only
readonly fa = FA;
// HTML only for button enabling
inProgressStatus: PentestStatus = PentestStatus.IN_PROGRESS;
columns: Array<FindingColumns> = [
FindingColumns.TITLE, FindingColumns.SEVERITY, FindingColumns.DESCRIPTION, FindingColumns.IMPACT, FindingColumns.ACTIONS
];
dataSource: NbTreeGridDataSource<FindingEntry>;
data: FindingEntry[] = [];
getters: NbGetters<FindingEntry, FindingEntry> = {
dataGetter: (node: FindingEntry) => node,
childrenGetter: (node: FindingEntry) => node.childEntries || undefined,
expandedGetter: (node: FindingEntry) => !!node.expanded,
};
ngOnInit(): void {
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 {
this.findingService.getFindingsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
.pipe(
untilDestroyed(this),
/*filter(isNotNullOrUndefined),*/
tap(() => this.loading$.next(true))
)
.subscribe({
next: (findings: Finding[]) => {
// ToDo: Handle this case before in pipe
if (findings) {
this.data = transformFindingsToObjectiveEntries(findings);
} else {
this.data = [];
}
this.dataSource.setData(this.data, this.getters);
this.loading$.next(false);
},
error: err => {
console.error(err);
// ToDo: Implement again after proper lazy loading and routing
// this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE);
this.loading$.next(false);
}
});
}
onClickAddFinding(): void {
this.findingDialogService.openFindingDialog(
FindingDialogComponent,
null,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
},
this.pentestInfo$.getValue()
).pipe(
untilDestroyed(this)
).subscribe({
next: (newFinding: Finding) => {
this.loadFindingsData();
}
});
}
onClickEditFinding(findingEntry): void {
this.findingService.getFindingById(findingEntry.data.findingId).pipe(
filter(isNotNullOrUndefined),
untilDestroyed(this)
).subscribe({
next: (existingFinding: Finding) => {
if (existingFinding) {
this.findingDialogService.openFindingDialog(
FindingDialogComponent,
existingFinding,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
},
this.pentestInfo$.getValue()
).pipe(
untilDestroyed(this)
).subscribe({
next: (updatedFinding: Finding) => {
this.loadFindingsData();
}
});
} else {
this.notificationService.showPopup('finding.popup.not.available', PopupType.INFO);
}
},
error: err => {
console.error(err);
}
});
}
onClickDeleteFinding(findingEntry): void {
const message = {
title: 'finding.delete.title',
key: 'finding.delete.key',
data: {name: findingEntry.data.title},
};
this.dialogService.openConfirmDialog(
message
).onClose.pipe(
untilDestroyed(this)
).subscribe({
next: () => {
this.deleteFinding(findingEntry);
}
});
}
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
private deleteFinding(findingEntry): void {
this.findingService.deleteFindingByPentestAndFindingId(
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
findingEntry.data.findingId)
.pipe(
untilDestroyed(this)
).subscribe({
next: (deletedFinding: any) => {
this.store.dispatch(new UpdatePentestFindings(deletedFinding.id));
this.loadFindingsData();
this.notificationService.showPopup('finding.popup.delete.success', PopupType.SUCCESS);
}, error: error => {
console.error(error);
this.onRequestFailed(findingEntry);
this.notificationService.showPopup('finding.popup.delete.failed', PopupType.FAILURE);
}
});
}
private onRequestFailed(retryParameter: any): void {
this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose
.pipe(
untilDestroyed(this)
)
.subscribe((ref) => {
if (ref.retry) {
this.deleteFinding(retryParameter);
}
});
}
}
enum FindingColumns {
FINDING_ID = 'findingId',
TITLE = 'title',
SEVERITY = 'severity',
DESCRIPTION = 'description',
IMPACT = 'impact',
ACTIONS = 'actions'
}

View File

@ -0,0 +1,11 @@
<div class="pentest-info">
<h4>
{{ getPentestHeaderForObjective(pentestInfo$.getValue().refNumber) | translate}}
</h4>
<div class="description">
<div>
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
</div>
</div>
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
</div>

View File

@ -0,0 +1,16 @@
.pentest-info {
overflow: hidden !important;
position: relative !important;
.description {
// ToDo: Make only description scrollable
// Scrollbar
overflow-y: scroll !important;
overflow-x: hidden;
scroll-behavior: smooth;
width: 60vw;
font-size: 1rem;
white-space: pre-line;
}
}

View File

@ -0,0 +1,102 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestInfoComponent} from './pentest-info.component';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentIds: []
},
};
describe('PentestInfoComponent', () => {
let component: PentestInfoComponent;
let fixture: ComponentFixture<PentestInfoComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestInfoComponent
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestInfoComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
component.pentestInfo$.next({
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: []
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import {Component, Input, OnInit} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {Pentest} from '@shared/models/pentest.model';
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 {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Store} from '@ngxs/store';
@UntilDestroy()
@Component({
selector: 'app-pentest-info',
templateUrl: './pentest-info.component.html',
styleUrls: ['./pentest-info.component.scss']
})
export class PentestInfoComponent implements OnInit {
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
constructor(private store: Store) { }
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 {
return getTitleKeyForRefNumber(refNumber);
}
getPentestInfoForObjective(refNumber: string): string {
return getPentestInfoForObjective(refNumber);
}
}

View File

@ -0,0 +1,41 @@
<div class="pentest-header" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="space-between center">
<div class="exit-button-container">
<button nbButton
shape="round"
title="{{ 'global.action.exit' | translate }}"
(click)="onClickRouteBack()">
<fa-icon [icon]="fa.faLongArrowAltLeft"
class="exit-element-icon fa-lg">
</fa-icon>
<span class="exit-element-text"> {{ 'global.action.exit' | translate }} </span>
</button>
</div>
<div class="header-info" fxLayout="row" fxHide.lt-lg>
<span class="project-title">{{selectedProjectTitle$.getValue()}}</span>
<span class="pentest-ref">{{" / " + pentest$.getValue().refNumber}}</span>
</div>
<div class="header-info-mobile" fxHide fxShow.lt-lg>
<span class="pentest-ref">{{pentest$.getValue().refNumber}}</span>
</div>
<div class="pentest-status-container" fxLayout="row" fxLayoutGap="2.5rem" fxLayoutAlign="end center">
<!-- Pentest Timer-->
<div class="timer-component">
<app-timer></app-timer>
</div>
<!-- Complete Pentest -->
<div>
<button nbButton
class="complete-pentest-button"
status="success"
[disabled]="!pentestStatusChanged() || !pentestHasFindingsOrComments()"
title="{{ 'global.action.save' | translate }}"
(click)="onClickCompletePentest()">
<fa-icon [icon]="fa.faSquare"></fa-icon>
<span class="action-element-text"> {{ 'global.action.complete' | translate }} </span>
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,94 @@
@import '../../../assets/@theme/styles/_text-overflow.scss';
.pentest-header {
width: 100vw;
.header-info {
position: absolute;
margin-left: 10rem;
margin-right: 10rem;
text-align: center;
.project-title {
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 2rem, $lines-to-show: 1, $max-width: 32rem);
}
.pentest-ref {
font-size: 1.5rem;
font-weight: bold;
line-height: 2rem;
}
}
.header-info-mobile{
position: absolute;
margin-left: 10rem;
margin-right: 10rem;
text-align: center;
.pentest-ref {
font-size: 1.5rem;
font-weight: bold;
line-height: 2rem;
}
}
.exit-button-container {
.exit-element-icon {
}
.exit-element-text {
padding-left: 0.5rem;
}
}
.pentest-status-container {
position: fixed;
right: 4rem;
// display: flex;
// align-content: flex-end;
.timer-component {
height: 2rem !important;
max-height: 2rem !important;
margin: 0.5rem 2.25rem 1rem 0;
}
.complete-pentest-button {
// position: absolute;
.action-element-text {
padding-left: 0.5rem;
}
}
.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 {
// height: 1rem !important;
margin: 1rem 0 1rem 0;
}
}
}

View File

@ -0,0 +1,91 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestHeaderComponent} from './pentest-header.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: [],
commentIds: []
},
};
describe('PentestHeaderComponent', () => {
let component: PentestHeaderComponent;
let fixture: ComponentFixture<PentestHeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PentestHeaderComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestHeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,177 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Route} from '@shared/models/route.enum';
import {Store} from '@ngxs/store';
import {Router} from '@angular/router';
import {ChangePentest} from '@shared/stores/project-state/project-state.actions';
import {BehaviorSubject} from 'rxjs';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {Project} from '@shared/models/project.model';
import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {PentestService} from '@shared/services/api/pentest.service';
import {StatusText} from '@shared/widgets/status-tag/status-tag.component';
@UntilDestroy()
@Component({
selector: 'app-pentest-header',
templateUrl: './pentest-header.component.html',
styleUrls: ['./pentest-header.component.scss']
})
export class PentestHeaderComponent implements OnInit, OnDestroy {
// HTML only
readonly fa = FA;
pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
selectedProjectTitle$: BehaviorSubject<string> = new BehaviorSubject<string>('');
pentestChanged$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
// Pentest Timer Handler
currentTimeSpent = 0;
private initialTimeSpent: number;
// 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.PAUSED, translationText: 'pentest.statusText.paused'},
{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,
private pentestService: PentestService,
private notificationService: NotificationService,
private readonly router: Router) {
}
ngOnInit(): void {
this.store.selectOnce(ProjectState.project).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedProject: Project) => {
this.selectedProjectId$.next(selectedProject.id);
this.selectedProjectTitle$.next(selectedProject?.title);
},
error: err => {
console.error(err);
}
});
this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
this.currentStatus = selectedPentest.status;
this.currentTimeSpent = selectedPentest.timeSpent ? selectedPentest.timeSpent : 0;
this.pentest$.next(selectedPentest);
},
error: err => {
console.error(err);
}
});
// Setup initial values for status and time outside of store subscription
this.initialPentestStatus = this.currentStatus;
this.initialTimeSpent = this.currentTimeSpent;
}
onClickRouteBack(): void {
// Route back to overview
this.router.navigate([Route.OBJECTIVE_OVERVIEW])
.then(
() => {
this.store.dispatch(new ChangePentest(null));
}
).finally();
}
onClickCompletePentest(): void {
// Update existing Pentest
this.pentest$.next({...this.pentest$.getValue(), status: PentestStatus.COMPLETED, timeSpent: this.currentTimeSpent});
this.updatePentest();
}
private updatePentest(): void {
this.pentestService.updatePentest(transformPentestToRequestBody(this.pentest$.getValue()))
.subscribe({
next: (pentest: Pentest) => {
this.store.dispatch(new ChangePentest(pentest));
this.initialTimeSpent = pentest.timeSpent;
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.initialTimeSpent !== this.currentTimeSpent && this.currentTimeSpent !== 0) {
this.pentestChanged$.next(true);
} else {
this.pentestChanged$.next(false);
}
return this.pentestChanged$.getValue();
}
/**
* @return true if pentest includes at least one finding or comment
*/
pentestHasFindingsOrComments(): boolean {
const pentest: Pentest = this.pentest$.getValue();
// Check if pentest includes any findings or comments
return pentest?.findingIds?.length > 0 || pentest?.commentIds?.length > 0;
}
/**
* @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.PAUSED: {
pentestFillStatus = 'info';
break;
}
case PentestStatus.IN_PROGRESS: {
pentestFillStatus = 'warning';
break;
}
case PentestStatus.COMPLETED: {
pentestFillStatus = 'success';
break;
}
default: {
pentestFillStatus = 'basic';
break;
}
}
return pentestFillStatus;
}
ngOnDestroy(): void {
if (this.pentestStatusChanged()) {
// Save current Pentest before exiting
this.pentest$.next({...this.pentest$.getValue(), status: PentestStatus.PAUSED, timeSpent: this.currentTimeSpent});
this.updatePentest();
}
}
}

View File

@ -0,0 +1,12 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PentestRoutingModule {
}

View File

@ -0,0 +1,15 @@
<div fxFlex class="pentest">
<nb-layout fxFlex>
<nb-layout-header fxFlex="0 1 max-content" class="stepper-column">
<app-pentest-header></app-pentest-header>
</nb-layout-header>
<nb-layout-column fxFlex="0 1 max-content" class="column-wrapper">
<nb-card class="content-column">
<nb-card-body>
<app-pentest-content></app-pentest-content>
</nb-card-body>
</nb-card>
</nb-layout-column>
</nb-layout>
</div>

View File

@ -0,0 +1,24 @@
@import '../../assets/@theme/styles/themes';
@import '../../assets/@theme/styles/variables';
.pentest {
width: 100vw;
height: calc(100vh - #{$header-height});
overflow: hidden;
.header-column {
width: 100vw;
height: $pentest-header-height;
}
.column-wrapper {
padding-left: 0 !important;
.content-column {
overflow: auto !important;
width: 100vw;
max-width: 100vw;
height: calc(100vh - #{$pentest-header-height} - #{$header-height});
}
}
}

View File

@ -0,0 +1,35 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestComponent} from './pentest.component';
import {NbLayoutModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {RouterTestingModule} from '@angular/router/testing';
describe('PentestComponent', () => {
let component: PentestComponent;
let fixture: ComponentFixture<PentestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestComponent
],
imports: [
NbLayoutModule,
ThemeModule.forRoot(),
RouterTestingModule.withRoutes([])
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pentest',
templateUrl: './pentest.component.html',
styleUrls: ['./pentest.component.scss']
})
export class PentestComponent implements OnInit {
constructor() { }
ngOnInit(): void {
// tslint:disable-next-line:no-console
}
}

View File

@ -0,0 +1,58 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {PentestComponent} from './pentest.component';
import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
import {PentestHeaderComponent} from './pentest-header/pentest-header.component';
import {PentestContentComponent} from './pentest-content/pentest-content.component';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core';
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
import {PentestInfoComponent} from './pentest-content/pentest-info/pentest-info.component';
import {PentestFindingsComponent} from './pentest-content/pentest-findings/pentest-findings.component';
import {PentestCommentsComponent} from './pentest-content/pentest-comments/pentest-comments.component';
import {CommonAppModule} from '../common-app.module';
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
import {CommentDialogModule} from '@shared/modules/comment-dialog/comment-dialog.module';
import {FindigWidgetModule} from '@shared/widgets/findig-widget/findig-widget.module';
import {TimerModule} from '@shared/modules/timer/timer.module';
@NgModule({
declarations: [
PentestComponent,
PentestHeaderComponent,
PentestContentComponent,
PentestInfoComponent,
PentestFindingsComponent,
PentestCommentsComponent
],
imports: [
CommonModule,
CommonAppModule,
RouterModule.forChild([{
path: '',
component: PentestComponent
}]),
NbLayoutModule,
NbCardModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
NbButtonModule,
StatusTagModule,
NbTabsetModule,
NbTreeGridModule,
SeverityTagModule,
NbSelectModule,
// Dialog Modules
FindingDialogModule,
CommentDialogModule,
FindigWidgetModule,
// Modules
TimerModule,
]
})
export class PentestModule {
}

View File

@ -1,16 +1,12 @@
import { NgModule } from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ProjectOverviewComponent} from './project-overview.component';
import {Route} from '@shared/models/route.enum';
const routes: Routes = [
{
path: '',
component: ProjectOverviewComponent
},
{
path: 'id',
path: Route.OBJECTIVE_OVERVIEW,
loadChildren: () => import('./project').then(mod => mod.ProjectModule),
},
}
];
@NgModule({

View File

@ -1,84 +1,68 @@
<div fxLayout="row" fxLayoutGap="2rem">
<div *ngFor="let project of projects | async">
<nb-card class="project-card" accent="success">
<nb-card-header fxLayoutAlign="start center"
routerLink="id"
fragment="{{project.id}}"
class="project-link project-header"
[state]="{selectedProject:project}">
<h4>{{project?.title}}</h4>
</nb-card-header>
<nb-card-body class="project-link"
routerLink="id"
fragment="{{project.id}}"
[state]="{selectedProject:project}">
<p class="project-subheader">
{{'project.client' | translate}}:
</p>
<span class="project-paragraph">
{{project?.client}}
</span>
<p class="project-subheader">
{{'project.tester' | translate}}:
</p>
<span class="project-paragraph">
{{project?.tester}}
</span>
<p class="project-subheader">
{{'project.createdAt' | translate}}:
</p>
<span class="project-paragraph">
{{project?.createdAt | dateTimeFormat}}
</span>
</nb-card-body>
<nb-card-footer>
<!--ToDo: Display correct progress of project-->
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start end">
<nb-progress-bar class="project-progress"
status="warning"
[value]="40"
[displayValue]="true">
</nb-progress-bar>
<button nbButton
status="primary"
size="small"
class="project-button"
(click)="onClickEditProject(project)">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<div fxFlex="0 1 max-content" fxLayout="column" class="pentest-overview">
<nb-layout fxFlex>
<!--Header-->
<nb-layout-header class="pentest-overview-header">
<div fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="space-between center">
<!--Filter-->
<div fxLayout="row" fxLayoutGap="1rem" class="header-filer">
<!--Actions-->
<form class="project-filter-input">
<nb-form-field>
<fa-icon nbPrefix class="search-prefix-icon" [icon]="fa.faSearch"></fa-icon>
<input type="search"
fullWidth nbInput
class="search-field"
[formControl]="projectSearch"
placeholder="{{ 'project.filter.placeholder' | translate: this.allProjectsCount$?.getValue() }}"
status="basic"
shape="semi-round"
fieldSize="medium">
</nb-form-field>
</form>
<!--ToDo: Add dropdown to filter for specific state-->
<button nbButton
status="danger"
size="small"
class="project-button"
(click)="onClickDeleteProject(project)">
<fa-icon [icon]="fa.faTrash"></fa-icon>
outline
size="medium"
shape="semi-round"
class="reset-filter-btn"
[disabled]="projectSearch.value === ''"
(click)="onClickResetFilter()">
<fa-icon [icon]="fa.faFilterCircleXmark" class="btn-icon"></fa-icon>
{{'global.action.reset' | translate}}
</button>
</div>
</nb-card-footer>
</nb-card>
</div>
<!--Button-->
<div class="header-project-button">
<button nbButton hero
status="info"
size="medium"
shape="round"
class="add-project-button"
(click)="onClickAddProject()">
<fa-icon [icon]="fa.faPlus" class="btn-icon"></fa-icon>
{{'project.overview.add.project' | translate}}
</button>
</div>
</div>
</nb-layout-header>
<!--Column-->
<!--ToDo: Fix the column style for multiple projects in css-->
<nb-layout-column class="pentest-overview-column">
<div class="project-grid">
<div class="project" *ngFor="let project of projects$ | async">
<app-project-widget [project]="project"></app-project-widget>
</div>
</div>
<!--Error Text-->
<div *ngIf="projects$.getValue() == null || projects$.getValue().length === 0 && loading$.getValue() === false"
fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'project.overview.no.projects' | translate}}
</p>
</div>
<!--Loading Spinner-->
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>
</nb-layout-column>
</nb-layout>
</div>
<div *ngIf="projects.getValue().length === 0 || !isLoading()" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'project.overview.no.projects' | translate}}
</p>
</div>
<div fxLayoutAlign="end end">
<button nbButton
status="primary"
size="large"
shape="round"
class="add-project-button"
(click)="onClickAddProject()">
<fa-icon [icon]="fa.faPlus" class="new-project-icon"></fa-icon>
{{'project.overview.add.project' | translate}}
</button>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -1,62 +1,88 @@
@import '../../assets/@theme/styles/themes';
@import '../../assets/@theme/styles/variables';
@import '../../assets/@theme/styles/_text-overflow.scss';
.project-card {
max-width: 22rem;
width: 22rem;
min-width: 20rem;
max-height: 100%;
height: 100%;
min-height: 100%;
.pentest-overview {
width: 100vw;
height: 85vh;
overflow: hidden !important;
.project-header {
max-height: 8rem;
height: 8rem;
min-height: 6rem;
.pentest-overview-header {
width: 100vw;
height: 5rem;
.header-filer {
.project-filter-input {
width: 24rem;
border-color: nb-theme(color-control-default);
.search-field:active {
color: nb-theme(color-info-default);
// opacity: initial;
}
.search-prefix-icon:active {
color: nb-theme(color-info-default);
}
}
.state-dialog {
margin-left: auto;
margin-right: 0;
.states {
width: 14rem !important;
}
}
.reset-filter-btn {
.btn-icon {
padding-right: 0.5rem;
}
}
}
.header-project-button {
position: fixed;
right: 1.5rem;
.add-project-button {
// align-content: flex-end;
margin: 6rem 2rem 6rem 0;
.btn-icon {
padding-right: 0.5rem;
}
}
}
}
.project-subheader {
font-size: 1.25rem;
font-weight: bold;
}
.pentest-overview-column {
width: 100vw;
// ToDo: Adjust this property when adding footer
height: calc(100% - 15rem) !important;
max-height: 100vh !important;
margin-top: 1.25rem;
// Scrollbar
overflow-y: scroll !important;
overflow-x: hidden;
scroll-behavior: smooth;
.project-paragraph {
font-size: 1.15rem;
font-style: italic;
}
.project-grid {
display: grid;
/* define the number of grid columns */
grid-template-columns: repeat( auto-fill, minmax(24rem, 1fr) );
.project-progress {
max-width: 65%;
width: 65%;
min-width: 65%;
}
.project {
padding-bottom: 1.25rem;
height: max-content;
}
}
.project-button {
height: 1.425rem;
.error-text {
font-size: 1.25rem;
font-weight: bold;
}
}
}
.project-card:hover {
background-color: nb-theme(color-basic-transparent-focus);
// Increases element size on hover
// Decreases usability which is why it is commented out
/*
margin-top: +0.625rem;
transform: scale(1.025);
*/
}
.project-link:hover {
cursor: pointer !important;
}
.add-project-button {
margin: 6rem 2rem 6rem 0;
.new-project-icon {
padding-right: 0.5rem;
}
}
.error-text {
font-size: 1.25rem;
font-weight: bold;
}

View File

@ -8,17 +8,17 @@ import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} from
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
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 {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {ProjectServiceMock} from '@shared/services/project.service.mock';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {KeycloakService} from 'keycloak-angular';
@ -26,6 +26,7 @@ import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {MockComponent} from 'ng-mocks';
describe('ProjectOverviewComponent', () => {
let component: ProjectOverviewComponent;
@ -35,20 +36,19 @@ describe('ProjectOverviewComponent', () => {
await TestBed.configureTestingModule({
declarations: [
ProjectOverviewComponent,
LoadingSpinnerComponent,
DateTimeFormatPipe
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
ProjectOverviewRoutingModule,
NbCardModule,
NbButtonModule,
FlexLayoutModule,
BrowserAnimationsModule,
FontAwesomeModule,
TranslateModule,
NbProgressBarModule,
ProjectOverviewRoutingModule,
NbSpinnerModule,
HttpClientTestingModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -58,16 +58,14 @@ describe('ProjectOverviewComponent', () => {
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState]),
HttpClientModule,
HttpClientTestingModule
NgxsModule.forRoot([SessionState])
],
providers: [
KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
{provide: NotificationService, useClass: NotificationServiceMock}
]
})
.compileComponents();

View File

@ -1,47 +1,88 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {Project} from '@shared/models/project.model';
import {BehaviorSubject, Observable} from 'rxjs';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {ProjectService} from '@shared/services/project.service';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {ProjectService} from '@shared/services/api/project.service';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {startWith, tap} from 'rxjs/operators';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {Router} from '@angular/router';
import {Store} from '@ngxs/store';
import {UntypedFormControl} from '@angular/forms';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {SetAvailableProjects} from '@shared/stores/project-state/project-state.actions';
@UntilDestroy()
@Component({
selector: 'app-project-overview',
templateUrl: './project-overview.component.html',
styleUrls: ['./project-overview.component.scss']
})
export class ProjectOverviewComponent implements OnInit, OnDestroy {
export class ProjectOverviewComponent implements OnInit {
// HTML only
readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
projects$: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
allProjects$: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
// Search
projectSearch: UntypedFormControl = new UntypedFormControl();
protected filter$: Observable<string>;
allProjectsCount$: BehaviorSubject<any> = new BehaviorSubject<any>({allProjectsCount: 0});
constructor(
private readonly projectService: ProjectService,
private readonly dialogService: DialogService,
private readonly projectDialogService: ProjectDialogService,
private readonly notificationService: NotificationService) {
private readonly notificationService: NotificationService,
private store: Store,
private router: Router,
private projectService: ProjectService,
private dialogService: DialogService,
private projectDialogService: ProjectDialogService) {
}
ngOnInit(): void {
// Load all available projects
this.loadProjects();
// Subscribe to project store
this.store.select(ProjectState.allProjects).pipe(
untilDestroyed(this)
).subscribe({
next: (projects: Project[]) => {
// tslint:disable-next-line:no-console
if (projects && projects.length === 0) {
this.loadProjects();
}
// Setup Search Form
this.projectSearch = new UntypedFormControl({value: '', disabled: projects?.length === 0});
this.setFilterObserverForProjects();
},
error: err => {
console.error(err);
},
});
}
loadProjects(): void {
this.projectService.getProjects()
.pipe(
untilDestroyed(this),
tap(() => this.loading$.next(true))
tap(() => this.loading$.next(true)),
untilDestroyed(this)
)
.subscribe({
next: (projects) => {
this.projects.next(projects);
next: (projects: Project[]) => {
if (projects) {
this.projects$.next(projects);
this.allProjects$.next(projects);
this.allProjectsCount$.next({allProjectsCount: projects.length});
this.store.dispatch(new SetAvailableProjects(projects));
} else {
this.projects$.next([]);
this.allProjects$.next([]);
this.allProjectsCount$.next({allProjectsCount: 0});
}
this.loading$.next(false);
},
error: err => {
@ -59,83 +100,50 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy {
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
autoFocus: true,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
mergeMap((value: ProjectDialogBody) => this.projectService.saveProject(value)),
untilDestroyed(this)
).subscribe({
next: () => {
this.loadProjects();
this.notificationService.showPopup('project.popup.save.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
this.notificationService.showPopup('project.popup.save.failed', PopupType.FAILURE);
}
});
}
onClickEditProject(project: Project): void {
this.projectDialogService.openProjectDialog(
ProjectDialogComponent,
project,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
mergeMap((value: ProjectDialogBody) => this.projectService.updateProject(project.id, value)),
untilDestroyed(this)
).subscribe({
next: () => {
this.loadProjects();
this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE);
}
});
}
onClickDeleteProject(project: Project): void {
const message = {
title: 'project.delete.title',
key: 'project.delete.key',
data: {name: project.title},
};
this.dialogService.openConfirmDialog(
message
).onClose.pipe(
filter((confirm) => !!confirm),
switchMap(() => this.projectService.deleteProjectById(project.id)),
catchError(() => {
this.notificationService.showPopup('project.popup.delete.failed', PopupType.FAILURE);
return [];
}),
untilDestroyed(this)
).subscribe({
next: () => {
this.loadProjects();
this.notificationService.showPopup('project.popup.delete.success', PopupType.SUCCESS);
}, error: error => {
console.error(error);
}
});
}
// HTML only
isLoading(): Observable<boolean> {
return this.loading$.asObservable();
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
onClickResetFilter(): void {
this.projectSearch.reset('');
this.projects$.next(this.allProjects$.getValue());
}
private setFilterObserverForProjects(): void {
this.filter$ = this.projectSearch.valueChanges.pipe(startWith(''));
this.filter$.subscribe(
(filterString: string) => {
if (filterString.length === 0) {
this.projects$.next(this.allProjects$.getValue());
} else {
const matchingProjects: Project[] = [];
this.allProjects$.getValue().forEach(project => {
// Project attributes that the user can filter through
if (
project.title.toLowerCase().includes(filterString.toLowerCase())
|| project.client.toLowerCase().includes(filterString.toLowerCase())
|| project.tester.toLowerCase().includes(filterString.toLowerCase())
|| project.state.toString().toLowerCase().includes(filterString.toLowerCase())
) {
matchingProjects.push(project);
}
});
this.projects$.next(matchingProjects);
}
}
);
}
}

View File

@ -2,34 +2,55 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProjectOverviewComponent} from './project-overview.component';
import {ProjectOverviewRoutingModule} from './project-overview-routing.module';
import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} from '@nebular/theme';
import {
NbButtonModule,
NbCardModule,
NbFormFieldModule,
NbInputModule,
NbLayoutModule,
NbProgressBarModule,
NbSelectModule
} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core';
import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe';
import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {CommonAppModule} from '../common-app.module';
import {ConfirmDialogModule} from '@shared/modules/confirm-dialog/confirm-dialog.module';
import {SecurityConfirmDialogModule} from '@shared/modules/security-confirm-dialog/security-confirm-dialog.module';
import {RouterModule} from '@angular/router';
import {ReportStateTagModule} from '@shared/widgets/report-state-tag/report-state-tag.module';
import {ReactiveFormsModule} from '@angular/forms';
import {ProjectWidgetModule} from '@shared/widgets/project-widget/project-widget.module';
@NgModule({
declarations: [
ProjectOverviewComponent,
DateTimeFormatPipe,
LoadingSpinnerComponent
ProjectOverviewComponent
],
imports: [
CommonModule,
CommonAppModule,
RouterModule.forChild([{
path: '',
component: ProjectOverviewComponent
}]),
NbCardModule,
NbButtonModule,
NbSpinnerModule,
NbProgressBarModule,
ProjectOverviewRoutingModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
ProjectDialogModule
],
exports: [
LoadingSpinnerComponent
ProjectDialogModule,
ConfirmDialogModule,
ReportStateTagModule,
SecurityConfirmDialogModule,
NbLayoutModule,
NbInputModule,
NbFormFieldModule,
ReactiveFormsModule,
NbSelectModule,
ProjectWidgetModule
]
})
export class ProjectOverviewModule {

View File

@ -1,11 +1,11 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ProjectComponent} from './project.component';
import {Route} from '@shared/models/route.enum';
const routes: Routes = [
{
path: '',
component: ProjectComponent
path: Route.PENTEST_OBJECTIVE,
loadChildren: () => import('../../pentest').then(mod => mod.PentestModule),
},
];

View File

@ -1,22 +1,24 @@
<div fxFlex class="pentest-overview">
<nb-layout>
<nb-layout-header fxFlexAlign="center" class="header-column">
<app-pentest-header></app-pentest-header>
<nb-layout fxFlex>
<nb-layout-header fxFlex="0 1 max-content" class="header-column">
<app-objective-header></app-objective-header>
</nb-layout-header>
<nb-layout-column fxFlexFill fxLayout="row" fxLayoutGap="2rem">
<nb-layout-column fxFlex="0 1 max-content" class="column-wrapper">
<nb-card class="categories-column">
<nb-card-body>
<app-pentest-categories></app-pentest-categories>
</nb-card-body>
<app-objective-categories></app-objective-categories>
</nb-card>
</nb-layout-column>
<nb-layout-column fxFlex=" max-content" class="table-wrapper" >
<nb-card class="table-column">
<nb-card-body>
<app-pentest-table></app-pentest-table>
<app-objective-table></app-objective-table>
</nb-card-body>
</nb-card>
</nb-layout-column>
</nb-layout>
</div>

View File

@ -7,16 +7,25 @@
overflow: hidden;
.header-column {
width: 100%
width: 100vw;
height: $pentest-header-height;
}
.categories-column {
width: 20%;
height: calc(100% - #{$pentest-header-height});
.column-wrapper {
padding-left: 0 !important;
.categories-column {
height: calc(100vh - #{$pentest-header-height} - #{$header-height});
}
}
.table-column {
width: 80%;
height: calc(100% - #{$pentest-header-height});
.table-wrapper{
padding-right: 0 !important;
border-style: none;
.table-column {
overflow: auto !important;
height: calc(100vh - #{$pentest-header-height} - #{$header-height});
}
}
}

View File

@ -7,18 +7,66 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {NgxsModule, Store} from '@ngxs/store';
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 {PentestOverviewModule} from '../../pentest-overview';
import {ObjectiveOverviewModule} from '../../objective-overview';
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 {ProjectService} from '@shared/services/api/project.service';
import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
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 {Category} from '@shared/models/category.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';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
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',
summary: '',
state: ReportState.NEW,
version: '1.0',
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,
enabled: true,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('ProjectComponent', () => {
let component: ProjectComponent;
let fixture: ComponentFixture<ProjectComponent>;
let store: Store;
beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({
declarations: [
ProjectComponent
@ -27,7 +75,7 @@ describe('ProjectComponent', () => {
CommonModule,
NbLayoutModule,
NbCardModule,
PentestOverviewModule,
ObjectiveOverviewModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -37,12 +85,18 @@ describe('ProjectComponent', () => {
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState]),
NgxsModule.forRoot([ProjectState]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
KeycloakService
KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NbDialogRef, useValue: dialogSpy},
{provide: ExportReportDialogService, useClass: ExportReportDialogServiceMock},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: NotificationService, useClass: NotificationServiceMock}
]
})
.compileComponents();
@ -50,6 +104,11 @@ describe('ProjectComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ProjectComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,5 +1,12 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {Store} from '@ngxs/store';
import {Router} from '@angular/router';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Route} from '@shared/models/route.enum';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {Project} from '@shared/models/project.model';
@UntilDestroy()
@Component({
selector: 'app-project',
templateUrl: './project.component.html',
@ -7,9 +14,29 @@ import { Component, OnInit } from '@angular/core';
})
export class ProjectComponent implements OnInit {
constructor() { }
ngOnInit(): void {
constructor(
private store: Store,
private readonly router: Router) {
}
ngOnInit(): void {
this.store.select(ProjectState.project).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedProject: Project) => {
this.initProjectStore();
},
error: err => {
console.error(err);
}
});
}
private initProjectStore(): void {
this.router.navigate([Route.OBJECTIVE_OVERVIEW]).then(() => {
}, err => {
this.router.navigate([Route.PROJECT_OVERVIEW]);
console.error(err);
});
}
}

View File

@ -5,9 +5,9 @@ import {ProjectComponent} from './project.component';
import {NbCardModule, NbLayoutModule} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {TranslateModule} from '@ngx-translate/core';
import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module';
import {ProjectRoutingModule} from './project-routing.module';
import {PentestOverviewModule} from '../../pentest-overview';
import {ObjectiveOverviewModule} from '../../objective-overview';
import {CommonAppModule} from '../../common-app.module';
@NgModule({
declarations: [
@ -15,6 +15,7 @@ import {PentestOverviewModule} from '../../pentest-overview';
],
imports: [
CommonModule,
CommonAppModule,
NbCardModule,
NbLayoutModule,
RouterModule.forChild([{
@ -24,8 +25,10 @@ import {PentestOverviewModule} from '../../pentest-overview';
ProjectRoutingModule,
TranslateModule,
FlexLayoutModule,
ProjectDialogModule,
PentestOverviewModule
ObjectiveOverviewModule
],
exports: [
ProjectComponent
]
})
export class ProjectModule {

Some files were not shown because too many files have changed in this diff Show More