Compare commits

..

1 Commits

Author SHA1 Message Date
Marcel Haag 87a377d440 fix: adjusted README.md 2022-03-25 22:03:38 +01:00
483 changed files with 12398 additions and 70519 deletions

View File

@ -1,106 +0,0 @@
# 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

View File

@ -1,173 +0,0 @@
# 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

View File

@ -1,56 +0,0 @@
# 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)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,15 @@
#!/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 "
@ -14,29 +24,24 @@ ______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/
echo "-------------CLEAN UP Container---------------"
echo -e "\n"
#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
rm -r ${keycloakVolume}
docker rm -f c4po-keycloak
docker rm -f c4po-keycloak-postgres
docker rm -f c4po-db
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 ${compose} build c4po-api --build-arg JAR_FILE_API=./build/libs/security-c4po-api-0.0.1-SNAPSHOT.jar ### toggle for additional build args ###
docker-compose -f ${composeBackend} build
echo -e "\n"
echo " - Frontend: "
docker-compose -f ${compose} build c4po-angular
docker-compose -f ${composeFrontend} build
echo -e "\n"
# docker-compose -f ${compose} up
echo "------------Start Docker Container------------"
echo -e "\n"
docker-compose -f ${compose} up
docker-compose -f ${composeKeycloak} -f ${composeDatabase} -f ${composeBackend} -f ${composeFrontend} up
# docker-compose -f ${compose} up

View File

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

View File

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

View File

@ -1,5 +1,5 @@
# base image
FROM node:14
FROM node:12.13.1
# 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 NODE_ENV=development npm install
RUN NODE_ENV=development npm install -g @angular/cli@12.2.17
RUN npm install
RUN npm install -g @angular/cli@10.2.0
# add app
COPY . /app

View File

@ -1,6 +1,6 @@
# Security C4PO Angular
# SecurityC4poAngular
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.
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
## Development server
@ -16,19 +16,12 @@ 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 [Jest](https://jestjs.io/).
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Cypress](https://www.cypress.io/).
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
## Contributing
Pull requests are welcome. For major changes, please open an issue first
to discuss what you would like to change.
Please make sure to read our [contributing guideline](https://github.com/marcel-haag/security-c4po/blob/main/CONTRIBUTING.md).

View File

@ -22,37 +22,25 @@
"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",
"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"
"src/assets/@theme/styles/styles.scss"
],
"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": {
@ -65,32 +53,25 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "5mb",
"maximumError": "8mb"
"maximumWarning": "3mb",
"maximumError": "5mb"
},
{
"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",
@ -99,7 +80,7 @@
},
"configurations": {
"production": {
"browserTarget": "security-c4po-angular:build:development"
"browserTarget": "security-c4po-angular:build:production"
}
}
},
@ -112,9 +93,7 @@
"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",
@ -155,6 +134,7 @@
}
}
},
"defaultProject": "security-c4po-angular",
"cli": {
"analytics": false
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -11,72 +11,62 @@
},
"private": true,
"dependencies": {
"@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",
"@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",
"@ngx-translate/http-loader": "^6.0.0",
"@ngxs/storage-plugin": "^3.7.3",
"@ngxs/store": "^3.7.3",
"chart.js": "^4.2.1",
"deep-equal": "^2.0.5",
"@ngxs/store": "^3.7.0",
"eva-icons": "^1.1.3",
"font-awesome": "^4.7.0",
"i18n-iso-countries": "^6.8.0",
"i18n-iso-countries": "^6.2.2",
"jwt-decode": "^3.1.2",
"keycloak-angular": "^13.1.0",
"keycloak-js": "^18.0.0",
"keycloak-angular": "^8.1.0",
"keycloak-js": "^13.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-translate-testing": "^6.0.0",
"ngx-take-until-destroy": "^5.4.0",
"ngx-translate-testing": "^5.0.0",
"roboto-fontface": "^0.10.0",
"rxjs": "^7.8.0",
"tslib": "^2.3.1",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"uuid": "^8.3.1",
"zone.js": "~0.11.4"
"zone.js": "~0.10.2"
},
"devDependencies": {
"@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",
"@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",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.9.5"
},
"resolutions": {
"webpack": "^5.0.0"
},
"moduleDirectories": [
"src"
]
"typescript": "~4.0.2"
}
}

View File

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

View File

@ -2,31 +2,20 @@ 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 = Route.PROJECT_OVERVIEW;
export const START_PAGE = 'projects';
const routes: Routes = [
{
path: Route.HOME,
path: 'home',
component: HomeComponent,
canActivate: [AuthGuardService]
},
{
path: Route.PROJECT_OVERVIEW,
path: 'projects',
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',
@ -38,7 +27,7 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes, {})],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

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

View File

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

View File

@ -2,6 +2,7 @@ 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';
@ -26,6 +27,7 @@ describe('AppComponent', () => {
deps: [HttpClient]
}
}),
NbEvaIconsModule,
ThemeModule,
HeaderModule,
NgxsModule.forRoot([SessionState]),

View File

@ -5,14 +5,11 @@ 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 {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {SessionState, SessionStateModel} from '../shared/stores/session-state/session-state';
import {untilDestroyed} from 'ngx-take-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',
@ -26,8 +23,6 @@ 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();
}
@ -48,14 +43,10 @@ export class AppComponent implements OnInit, OnDestroy {
initApp(): void {
// for global language
this.translateService.use(this.localeId);
// for number, date and time
registerLocaleData(localeDe, 'de-DE');
// for font-awesome icons
this.nebularIconLibraries.registerFontPack('fas', { packClass: 'fas', iconClassPrefix: 'fa' });
this.nebularIconLibraries.registerFontPack('far', { packClass: 'far', iconClassPrefix: 'fa' });
this.nebularIconLibraries.registerFontPack('fab', { packClass: 'fab', iconClassPrefix: 'fa' });
this.nebularIconLibraries.setDefaultPack('far');
// for country codes
this.setupCountryCode();
}

View File

@ -6,37 +6,29 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {
NbLayoutModule,
NbToastrModule,
NbIconModule,
NbCardModule,
NbButtonModule,
NbSelectModule,
NbThemeModule,
NbOverlayContainerAdapter,
NbDialogModule, NbMenuModule, NbIconLibraries,
NbIconModule, NbCardModule, NbButtonModule, NbDialogService, NbDialogModule,
} 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/toaster-service/notification.service';
import {NotificationService} from '@shared/services/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 {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';
import {ConfirmDialogModule} from '@shared/modules/confirm-dialog/confirm-dialog.module';
import {OverlayContainer} from '@angular/cdk/overlay';
@NgModule({
declarations: [
@ -47,22 +39,17 @@ import {far} from '@fortawesome/free-regular-svg-icons';
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(),
NbMenuModule.forRoot(),
NbSelectModule,
NgxsModule.forRoot([SessionState, ProjectState], {developmentMode: !environment.production}),
NgxsLoggerPluginModule.forRoot({developmentMode: !environment.production}),
NbEvaIconsModule,
ConfirmDialogModule,
NgxsModule.forRoot([SessionState], {developmentMode: !environment.production}),
HttpClientModule,
TranslateModule.forRoot({
loader: {
@ -73,7 +60,7 @@ import {far} from '@fortawesome/free-regular-svg-icons';
}),
HeaderModule,
HomeModule,
RetryDialogModule
FlexLayoutModule
],
providers: [
HttpClient,
@ -83,22 +70,21 @@ import {far} from '@fortawesome/free-regular-svg-icons';
multi: true,
deps: [KeycloakService]
},
OverlayContainer,
KeycloakService,
httpInterceptorProviders,
NotificationService,
DialogService,
{provide: NbOverlayContainerAdapter, useClass: CustomOverlayContainer}
NbDialogService,
],
bootstrap: [
AppComponent
]
})
export class AppModule {
constructor(library: FaIconLibrary, faConfig: FaConfig, libraries: NbIconLibraries) {
library.addIconPacks(far, fas);
libraries.registerFontPack('solid', {packClass: 'fas', iconClassPrefix: 'fa'});
constructor(library: FaIconLibrary, faConfig: FaConfig) {
library.addIconPacks(fas, far);
faConfig.defaultPrefix = 'fas';
libraries.setDefaultPack('solid');
}
}

View File

@ -6,27 +6,22 @@ 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/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';
import {NotificationService} from '../shared/services/notification.service';
import {NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '../assets/@theme/theme.module';
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http);
}
@NgModule({
declarations: [
LoadingSpinnerComponent
],
declarations: [],
imports: [
CommonModule,
NbToastrModule, // used for notification service
NbSpinnerModule,
FontAwesomeModule,
FlexLayoutModule,
ThemeModule.forRoot(),
NbMenuModule.forRoot(),
FlexModule,
HttpClientModule,
TranslateModule.forChild({
@ -39,11 +34,9 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
],
providers: [
HttpClient,
NotificationService,
NbOverlayContainerAdapter
NotificationService
],
exports: [
LoadingSpinnerComponent,
// modules
MomentModule
]

View File

@ -1,50 +1,26 @@
<div class="header" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="2rem">
<div class="header" fxLayout="row" fxLayoutAlign="center 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/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem"
height="60rem">
<img src="../../assets/images/favicons/corporate_favicon.ico" alt="logo light" class="header-icon" width="60rem" height="60rem">
</ng-template>
<div class="logo-container">
<h1>{{SECURITYC4PO_TITLE}} </h1>
<div class="logo-container" fxLayoutAlign="center center">
<h1 >{{SECURITYC4PO_TITLE}} </h1>
</div>
<div class="filler"></div>
<div fxLayoutGap="4rem">
<div fxLayoutAlign="end" fxLayoutGap="4rem">
<nb-actions size="medium">
<!--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>
<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>
<ng-template #changeIcon>
<fa-icon title="Lighttheme" [icon]="fa.faSun" class="fa-2x"></fa-icon>
<fa-icon [icon]="fa.faSun" class="new-element-icon"></fa-icon>
</ng-template>
</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>
</button>
</nb-action>
</nb-actions>
</div>
</div>

View File

@ -1,37 +1,6 @@
@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;
@ -42,6 +11,11 @@
align-items: center;
width: auto;
.logo-container {
font-style: oblique;
color: #e74c3c;
}
nb-action {
height: auto;
display: flex;

View File

@ -3,32 +3,16 @@ 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, NbMenuModule, NbMenuService, NbSelectModule} from '@nebular/theme';
import {NbActionsModule} 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({
@ -38,10 +22,7 @@ describe('HeaderComponent', () => {
imports: [
CommonModule,
NbActionsModule,
NbSelectModule,
FontAwesomeTestingModule,
HttpClientTestingModule,
NbMenuModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -50,24 +31,14 @@ describe('HeaderComponent', () => {
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState])
],
providers: [
{provide: DialogService, useClass: DialogServiceMock},
NbMenuService,
KeycloakService
RouterTestingModule.withRoutes([])
]
}).compileComponents();
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,141 +1,31 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
import {filter, map} from 'rxjs/operators';
import {NbThemeService} from '@nebular/theme';
import {map} from 'rxjs/operators';
import {untilDestroyed} from 'ngx-take-until-destroy';
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']
})
@UntilDestroy()
export class HeaderComponent implements OnInit {
export class HeaderComponent implements OnInit, OnDestroy {
// HTML only
readonly fa = FA;
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
// Menu only
readonly settingsIcon = 'gear';
readonly logoutIcon = 'right-from-bracket';
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
currentTheme = '';
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) {
}
constructor(private themeService: NbThemeService) { }
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((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();
.subscribe(themeName => this.currentTheme = themeName);
}
onClickSwitchTheme(): void {
@ -146,19 +36,8 @@ export class HeaderComponent implements OnInit {
}
}
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);
});
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
}

View File

@ -1,19 +1,9 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from './header.component';
import {
NbActionsModule,
NbButtonModule,
NbCardModule,
NbContextMenuModule,
NbSelectModule,
NbUserModule
} from '@nebular/theme';
import {NbActionsModule, NbButtonModule, NbCardModule} 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: [
@ -28,15 +18,7 @@ import {TutorialDialogModule} from '@shared/modules/tutorial-dialog/tutorial-dia
FontAwesomeModule,
NbCardModule,
NbActionsModule,
FlexLayoutModule,
NbSelectModule,
TranslateModule,
NbUserModule,
NbContextMenuModule,
ProfileSettingsModule,
TutorialDialogModule
],
providers: [
FlexLayoutModule
]
})
export class HeaderModule {

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import {Component, OnInit} from '@angular/core';
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {Store} from '@ngxs/store';
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {NotificationService, PopupType} from '../../shared/services/notification.service';
import {untilDestroyed} from 'ngx-take-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 {
export class LoginComponent implements OnInit, OnDestroy {
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 {
version: string;
// form control elements
loginFormGroup: UntypedFormGroup;
loginFormGroup: FormGroup;
loginUsernameCtrl: AbstractControl;
loginPasswordCtrl: AbstractControl;
@ -39,7 +39,7 @@ export class LoginComponent implements OnInit {
formCtrlStatus = FieldStatus.BASIC;
constructor(private fb: UntypedFormBuilder,
constructor(private fb: FormBuilder,
private router: Router,
private store: Store,
private readonly httpClient: HttpClient,
@ -108,6 +108,11 @@ export class LoginComponent implements OnInit {
return ctrlValue === '';
}
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
readAppVersion(): void {
this.httpClient.get<Version>('assets/version.json', {responseType: 'json'})
.subscribe((data: Version) => {

View File

@ -4,7 +4,7 @@ import {LoginComponent} from './login.component';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationService} from '../../shared/services/notification.service';
import {LoginRoutingModule} from './login-routing.module';
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme';
import {ReactiveFormsModule} from '@angular/forms';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,111 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ObjectiveHeaderComponent} from './objective-header.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbActionsModule, NbIconModule, NbMenuService} from '@nebular/theme';
import {ProjectService} from '@shared/services/api/project.service';
import {ProjectServiceMock} from '@shared/services/api/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
import {ExportReportDialogServiceMock} from '@shared/modules/export-report-dialog/service/export-report-dialog.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('ObjectiveHeaderComponent', () => {
let component: ObjectiveHeaderComponent;
let fixture: ComponentFixture<ObjectiveHeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ObjectiveHeaderComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
ThemeModule.forRoot(),
FontAwesomeModule,
NbIconModule,
NbActionsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
NbMenuService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: ExportReportDialogService, useClass: ExportReportDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveHeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<p>pentest-categories works!</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<p>pentest-table works!</p>

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoadingBarComponent } from './loading-bar.component';
import { PentestTableComponent } from './pentest-table.component';
describe('LoadingBarComponent', () => {
let component: LoadingBarComponent;
let fixture: ComponentFixture<LoadingBarComponent>;
describe('PentestTableComponent', () => {
let component: PentestTableComponent;
let fixture: ComponentFixture<PentestTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoadingBarComponent ]
declarations: [ PentestTableComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoadingBarComponent);
fixture = TestBed.createComponent(PentestTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,109 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestCommentsComponent} from './pentest-comments.component';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NgxsModule, Store} from '@ngxs/store';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('PentestCommentsComponent', () => {
let component: PentestCommentsComponent;
let fixture: ComponentFixture<PentestCommentsComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestCommentsComponent,
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
NbButtonModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestCommentsComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

@ -1,93 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestContentComponent} from './pentest-content.component';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: []
},
};
describe('PentestContentComponent', () => {
let component: PentestContentComponent;
let fixture: ComponentFixture<PentestContentComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestContentComponent
],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestContentComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

@ -1,110 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestFindingsComponent} from './pentest-findings.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {NbButtonModule, NbTreeGridModule} from '@nebular/theme';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {CommonModule} from '@angular/common';
import {MockComponent} from 'ng-mocks';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentIds: []
},
};
describe('PentestFindingsComponent', () => {
let component: PentestFindingsComponent;
let fixture: ComponentFixture<PentestFindingsComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestFindingsComponent,
MockComponent(LoadingSpinnerComponent)
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
NbButtonModule,
NbTreeGridModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestFindingsComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
component.pentestInfo$.next(DESIRED_PROJECT_STATE_SESSION.selectedPentest);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

@ -1,102 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestInfoComponent} from './pentest-info.component';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {ThemeModule} from '@assets/@theme/theme.module';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentIds: []
},
};
describe('PentestInfoComponent', () => {
let component: PentestInfoComponent;
let fixture: ComponentFixture<PentestInfoComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
PentestInfoComponent
],
imports: [
CommonModule,
BrowserAnimationsModule,
HttpClientTestingModule,
FontAwesomeModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
NgxsModule.forRoot([ProjectState])
],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestInfoComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
component.pentestInfo$.next({
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: []
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

@ -1,91 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PentestHeaderComponent} from './pentest-header.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NotificationServiceMock} from '@shared/services/toaster-service/notification.service.mock';
import {ReportState} from '@shared/models/state.enum';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
allProjects: [],
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
state: ReportState.NEW,
version: '1.0',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
enabled: true,
findingIds: [],
commentIds: []
},
};
describe('PentestHeaderComponent', () => {
let component: PentestHeaderComponent;
let fixture: ComponentFixture<PentestHeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PentestHeaderComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PentestHeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +1,84 @@
<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-->
<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>
<button nbButton
status="danger"
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}}
size="small"
class="project-button"
(click)="onClickDeleteProject(project)">
<fa-icon [icon]="fa.faTrash"></fa-icon>
</button>
</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>
</nb-card-footer>
</nb-card>
</div>
</div>
<div *ngIf="projects.getValue().length === 0 || !isLoading()" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'project.overview.no.projects' | translate}}
</p>
</div>
<div fxLayoutAlign="end end">
<button nbButton
status="primary"
size="large"
shape="round"
class="add-project-button"
(click)="onClickAddProject()">
<fa-icon [icon]="fa.faPlus" class="new-project-icon"></fa-icon>
{{'project.overview.add.project' | translate}}
</button>
</div>
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -1,88 +1,62 @@
@import '../../assets/@theme/styles/themes';
@import '../../assets/@theme/styles/variables';
@import '../../assets/@theme/styles/_text-overflow.scss';
.pentest-overview {
width: 100vw;
height: 85vh;
overflow: hidden !important;
.project-card {
max-width: 22rem;
width: 22rem;
min-width: 20rem;
max-height: 100%;
height: 100%;
min-height: 100%;
.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-header {
max-height: 8rem;
height: 8rem;
min-height: 6rem;
}
.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-subheader {
font-size: 1.25rem;
font-weight: bold;
}
.project-grid {
display: grid;
/* define the number of grid columns */
grid-template-columns: repeat( auto-fill, minmax(24rem, 1fr) );
.project-paragraph {
font-size: 1.15rem;
font-style: italic;
}
.project {
padding-bottom: 1.25rem;
height: max-content;
}
}
.project-progress {
max-width: 65%;
width: 65%;
min-width: 65%;
}
.error-text {
font-size: 1.25rem;
font-weight: bold;
}
.project-button {
height: 1.425rem;
}
}
.project-card:hover {
background-color: nb-theme(color-basic-transparent-focus);
// Increases element size on hover
// Decreases usability which is why it is commented out
/*
margin-top: +0.625rem;
transform: scale(1.025);
*/
}
.project-link:hover {
cursor: pointer !important;
}
.add-project-button {
margin: 6rem 2rem 6rem 0;
.new-project-icon {
padding-right: 0.5rem;
}
}
.error-text {
font-size: 1.25rem;
font-weight: bold;
}

View File

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

View File

@ -1,88 +1,47 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {Project} from '@shared/models/project.model';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {BehaviorSubject, Observable} from 'rxjs';
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 {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 {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 {
// HTML only
export class ProjectOverviewComponent implements OnInit, OnDestroy {
readonly fa = FA;
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
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});
projects: BehaviorSubject<Project[]> = new BehaviorSubject<Project[]>([]);
constructor(
private readonly notificationService: NotificationService,
private store: Store,
private router: Router,
private projectService: ProjectService,
private dialogService: DialogService,
private projectDialogService: ProjectDialogService) {
private readonly projectService: ProjectService,
private readonly dialogService: DialogService,
private readonly projectDialogService: ProjectDialogService,
private readonly notificationService: NotificationService) {
}
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(
tap(() => this.loading$.next(true)),
untilDestroyed(this)
untilDestroyed(this),
tap(() => this.loading$.next(true))
)
.subscribe({
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});
}
next: (projects) => {
this.projects.next(projects);
this.loading$.next(false);
},
error: err => {
@ -100,50 +59,83 @@ export class ProjectOverviewComponent implements OnInit {
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
autoFocus: false,
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();
}
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);
}
}
);
ngOnDestroy(): void {
// This method must be present when using ngx-take-until-destroy
// even when empty
}
}

View File

@ -2,55 +2,34 @@ 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,
NbFormFieldModule,
NbInputModule,
NbLayoutModule,
NbProgressBarModule,
NbSelectModule
} from '@nebular/theme';
import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} 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 {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';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
@NgModule({
declarations: [
ProjectOverviewComponent
ProjectOverviewComponent,
DateTimeFormatPipe,
LoadingSpinnerComponent
],
imports: [
CommonModule,
CommonAppModule,
RouterModule.forChild([{
path: '',
component: ProjectOverviewComponent
}]),
NbCardModule,
NbButtonModule,
NbSpinnerModule,
NbProgressBarModule,
ProjectOverviewRoutingModule,
FlexLayoutModule,
FontAwesomeModule,
TranslateModule,
ProjectDialogModule,
ConfirmDialogModule,
ReportStateTagModule,
SecurityConfirmDialogModule,
NbLayoutModule,
NbInputModule,
NbFormFieldModule,
ReactiveFormsModule,
NbSelectModule,
ProjectWidgetModule
ProjectDialogModule
],
exports: [
LoadingSpinnerComponent
]
})
export class ProjectOverviewModule {

View File

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

View File

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

View File

@ -7,25 +7,16 @@
overflow: hidden;
.header-column {
width: 100vw;
height: $pentest-header-height;
width: 100%
}
.column-wrapper {
padding-left: 0 !important;
.categories-column {
height: calc(100vh - #{$pentest-header-height} - #{$header-height});
}
.categories-column {
width: 20%;
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});
}
.table-column {
width: 80%;
height: calc(100% - #{$pentest-header-height});
}
}

View File

@ -7,66 +7,18 @@ 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, Store} from '@ngxs/store';
import {NgxsModule} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NbCardModule, NbDialogRef, NbLayoutModule} from '@nebular/theme';
import {NbCardModule, NbLayoutModule} from '@nebular/theme';
import {KeycloakService} from 'keycloak-angular';
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']
},
};
import {PentestOverviewModule} from '../../pentest-overview';
describe('ProjectComponent', () => {
let component: ProjectComponent;
let fixture: ComponentFixture<ProjectComponent>;
let store: Store;
beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({
declarations: [
ProjectComponent
@ -75,7 +27,7 @@ describe('ProjectComponent', () => {
CommonModule,
NbLayoutModule,
NbCardModule,
ObjectiveOverviewModule,
PentestOverviewModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -85,18 +37,12 @@ describe('ProjectComponent', () => {
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState]),
NgxsModule.forRoot([SessionState]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
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}
KeycloakService
]
})
.compileComponents();
@ -104,11 +50,6 @@ 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();
});

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