Compare commits
68 Commits
nsm_c4po_1
...
main
Author | SHA1 | Date |
---|---|---|
|
661cbe580d | |
|
8114abe9b6 | |
|
1888a98e08 | |
|
a5fa8ca9dd | |
|
930306d00f | |
|
dee3c35180 | |
|
306ee25085 | |
|
7055b7caf5 | |
|
056e4532fa | |
|
b6ec78ef49 | |
|
2f6fd7c2bc | |
|
bc8d59f1a9 | |
|
cf7204fb9d | |
|
6e55e61ce5 | |
|
061b93ff5e | |
|
65985e5ef3 | |
|
ed7c70c62d | |
|
b4bf6de8f8 | |
|
e0e23f7383 | |
|
07c6871294 | |
|
9e4fa27b92 | |
|
2c7ac85f6e | |
|
9fddff7740 | |
|
a37e06f8ca | |
|
3be43fa96e | |
|
4ca2828e0f | |
|
79a2493c37 | |
|
bb544c71a0 | |
|
280948c470 | |
|
88b3647295 | |
|
3c3f005537 | |
|
e27d6005db | |
|
40224635ab | |
|
cc182d932b | |
|
f5e34722f5 | |
|
6f209dfbb4 | |
|
16bc1a5d4f | |
|
46f79dcf89 | |
|
6a625349c8 | |
|
27a8e963e9 | |
|
076fa087e8 | |
|
a4536e9735 | |
|
e9aec4ec3e | |
|
b2f430f9fd | |
|
84b7c1a07d | |
|
de659e3293 | |
|
c1293b4da1 | |
|
f658073bcf | |
|
5d89467c1e | |
|
747cade495 | |
|
6764583481 | |
|
ca7018cad9 | |
|
b695b17a9e | |
|
ecaaeea079 | |
|
d814a87033 | |
|
5894722633 | |
|
93ccf3e034 | |
|
6ae4d4e62d | |
|
2cbb2d522b | |
|
6ec16c0cbf | |
|
4a2024dd69 | |
|
24dccb3e8f | |
|
ca77f6f208 | |
|
e80c0e8560 | |
|
f9ce7606f7 | |
|
abddd00451 | |
|
5d5dbe95fa | |
|
501a6d3427 |
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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.
|
94
README.md
94
README.md
|
@ -1,40 +1,94 @@
|
|||
# security-c4po
|
||||

|
||||

|
||||
[](https://owasp.org/other_projects/)<!-- @IGNORE PREVIOUS: link -->
|
||||
|
||||
### Chief Innovator
|
||||
> Daniel Mader
|
||||
|
||||
### Project Leads
|
||||
* Andreas Falk
|
||||
* Christina Paule
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||
[](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
|
||||
[](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
|
||||

|
||||
|
||||
## Data Structure
|
||||

|
||||
|
||||
## C4PO Roadmap
|
||||

|
||||
|
||||
## Project
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 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
|
||||

|
||||
|
||||
## Data Structure
|
||||

|
||||
* 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).
|
||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -44,3 +44,6 @@ testem.log
|
|||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Chache
|
||||
.angular/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
@import "../assets/@theme/styles/_variables.scss";
|
||||
|
||||
.header {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 100%;
|
||||
height: calc(100% - #{$header-height});
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '~@nebular/theme/styles/theming';
|
||||
@import '@nebular/theme/styles/theming';
|
||||
|
||||
$login-width: 24em;
|
||||
$input-width: 16rem;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export {ObjectiveOverviewModule} from './objective-overview.module';
|
||||
export {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module';
|
|
@ -0,0 +1,5 @@
|
|||
<div class="pentest-categories">
|
||||
<nb-menu id="category-menu" class="menu-style" tag="menu" [items]="categories"></nb-menu>
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@import '../../../assets/@theme/styles/themes';
|
||||
|
||||
.pentest-categories {
|
||||
width: 20rem;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export {PentestOverviewModule} from './pentest-overview.module';
|
||||
export {PentestOverviewRoutingModule} from './pentest-overview-routing.module';
|
|
@ -1 +0,0 @@
|
|||
<p>pentest-categories works!</p>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<p>header for "{{selectedProjectTitle}}" works!</p>
|
||||
</div>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<p>pentest-table works!</p>
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export {PentestModule} from './pentest.module';
|
||||
export {PentestRoutingModule} from './pentest-routing.module';
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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>
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue