Repository: softrams/bulwark Branch: master Commit: 341b3a62f53d Files: 276 Total size: 715.9 KB Directory structure: gitextract_flcbq_fg/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── agents/ │ │ └── my-agent.agent.md │ ├── dependabot.yml │ ├── pull_request_template.md │ ├── stale.yml │ └── workflows/ │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── .jest/ │ └── setEnvVars.js ├── .prettierignore ├── .prettierrc.json ├── .versionrc ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── TESTING.md ├── babel.config.js ├── buildspec.yaml ├── bulwark_base/ │ ├── Dockerfile │ ├── buildspec.yaml │ └── bulwark-entrypoint ├── commitlint.config.js ├── docker-compose.yml ├── frontend/ │ ├── .browserslistrc │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── e2e/ │ │ ├── protractor.conf.js │ │ ├── src/ │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.e2e.json │ ├── package.json │ ├── src/ │ │ ├── app/ │ │ │ ├── admin.guard.spec.ts │ │ │ ├── admin.guard.ts │ │ │ ├── administration/ │ │ │ │ ├── administration.component.html │ │ │ │ ├── administration.component.sass │ │ │ │ ├── administration.component.spec.ts │ │ │ │ ├── administration.component.ts │ │ │ │ ├── invite-user/ │ │ │ │ │ ├── invite-user.component.html │ │ │ │ │ ├── invite-user.component.sass │ │ │ │ │ ├── invite-user.component.spec.ts │ │ │ │ │ └── invite-user.component.ts │ │ │ │ └── settings/ │ │ │ │ ├── settings.component.html │ │ │ │ ├── settings.component.sass │ │ │ │ ├── settings.component.spec.ts │ │ │ │ └── settings.component.ts │ │ │ ├── alert/ │ │ │ │ ├── alert/ │ │ │ │ │ ├── alert.component.html │ │ │ │ │ ├── alert.component.sass │ │ │ │ │ ├── alert.component.spec.ts │ │ │ │ │ └── alert.component.ts │ │ │ │ ├── alert.module.ts │ │ │ │ ├── alert.service.spec.ts │ │ │ │ └── alert.service.ts │ │ │ ├── apikey-management/ │ │ │ │ ├── apikey-management.component.html │ │ │ │ ├── apikey-management.component.sass │ │ │ │ ├── apikey-management.component.spec.ts │ │ │ │ └── apikey-management.component.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.interceptor.ts │ │ │ ├── app.module.ts │ │ │ ├── app.service.spec.ts │ │ │ ├── app.service.ts │ │ │ ├── assessment-form/ │ │ │ │ ├── Assessment.ts │ │ │ │ ├── assessment-form.component.html │ │ │ │ ├── assessment-form.component.sass │ │ │ │ ├── assessment-form.component.spec.ts │ │ │ │ └── assessment-form.component.ts │ │ │ ├── assessments/ │ │ │ │ ├── assessments.component.html │ │ │ │ ├── assessments.component.sass │ │ │ │ ├── assessments.component.spec.ts │ │ │ │ └── assessments.component.ts │ │ │ ├── asset-form/ │ │ │ │ ├── Asset.ts │ │ │ │ ├── Jira.ts │ │ │ │ ├── asset-form.component.html │ │ │ │ ├── asset-form.component.sass │ │ │ │ ├── asset-form.component.spec.ts │ │ │ │ └── asset-form.component.ts │ │ │ ├── auth.guard.spec.ts │ │ │ ├── auth.guard.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── classes/ │ │ │ │ ├── Alert.ts │ │ │ │ ├── ProblemLocation.ts │ │ │ │ └── Resource.ts │ │ │ ├── dashboard/ │ │ │ │ ├── dashboard.component.html │ │ │ │ ├── dashboard.component.sass │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ └── dashboard.component.ts │ │ │ ├── email-validate/ │ │ │ │ ├── email-validate.component.html │ │ │ │ ├── email-validate.component.sass │ │ │ │ ├── email-validate.component.spec.ts │ │ │ │ └── email-validate.component.ts │ │ │ ├── enums/ │ │ │ │ └── roles.enum.ts │ │ │ ├── footer/ │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.sass │ │ │ │ ├── footer.component.spec.ts │ │ │ │ └── footer.component.ts │ │ │ ├── forgot-password/ │ │ │ │ ├── forgot-password.component.html │ │ │ │ ├── forgot-password.component.sass │ │ │ │ ├── forgot-password.component.spec.ts │ │ │ │ └── forgot-password.component.ts │ │ │ ├── global-manager.service.spec.ts │ │ │ ├── global-manager.service.ts │ │ │ ├── interfaces/ │ │ │ │ ├── ApiKey.ts │ │ │ │ ├── App_File.ts │ │ │ │ ├── Organization.ts │ │ │ │ ├── Screenshot.ts │ │ │ │ ├── Settings.ts │ │ │ │ ├── Team.ts │ │ │ │ ├── Tokens.ts │ │ │ │ └── User.ts │ │ │ ├── loader.service.spec.ts │ │ │ ├── loader.service.ts │ │ │ ├── login/ │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.sass │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ │ ├── navbar/ │ │ │ │ ├── navbar.component.html │ │ │ │ ├── navbar.component.sass │ │ │ │ ├── navbar.component.spec.ts │ │ │ │ └── navbar.component.ts │ │ │ ├── org-form/ │ │ │ │ ├── Organization.ts │ │ │ │ ├── org-form.component.html │ │ │ │ ├── org-form.component.sass │ │ │ │ ├── org-form.component.spec.ts │ │ │ │ └── org-form.component.ts │ │ │ ├── organization/ │ │ │ │ ├── organization.component.html │ │ │ │ ├── organization.component.sass │ │ │ │ ├── organization.component.spec.ts │ │ │ │ └── organization.component.ts │ │ │ ├── page-not-found/ │ │ │ │ ├── page-not-found.component.html │ │ │ │ ├── page-not-found.component.sass │ │ │ │ ├── page-not-found.component.spec.ts │ │ │ │ └── page-not-found.component.ts │ │ │ ├── password-reset/ │ │ │ │ ├── password-reset.component.html │ │ │ │ ├── password-reset.component.sass │ │ │ │ ├── password-reset.component.spec.ts │ │ │ │ └── password-reset.component.ts │ │ │ ├── register/ │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.sass │ │ │ │ ├── register.component.spec.ts │ │ │ │ └── register.component.ts │ │ │ ├── report/ │ │ │ │ ├── report.component.html │ │ │ │ ├── report.component.sass │ │ │ │ ├── report.component.spec.ts │ │ │ │ └── report.component.ts │ │ │ ├── team/ │ │ │ │ ├── team.component.html │ │ │ │ ├── team.component.sass │ │ │ │ ├── team.component.spec.ts │ │ │ │ └── team.component.ts │ │ │ ├── team-form/ │ │ │ │ ├── team-form.component.html │ │ │ │ ├── team-form.component.sass │ │ │ │ ├── team-form.component.spec.ts │ │ │ │ └── team-form.component.ts │ │ │ ├── team.service.spec.ts │ │ │ ├── team.service.ts │ │ │ ├── user-form/ │ │ │ │ ├── user-form.component.html │ │ │ │ ├── user-form.component.sass │ │ │ │ ├── user-form.component.spec.ts │ │ │ │ └── user-form.component.ts │ │ │ ├── user-management/ │ │ │ │ ├── user-management.component.html │ │ │ │ ├── user-management.component.sass │ │ │ │ ├── user-management.component.spec.ts │ │ │ │ └── user-management.component.ts │ │ │ ├── user-profile/ │ │ │ │ ├── user-profile.component.html │ │ │ │ ├── user-profile.component.sass │ │ │ │ ├── user-profile.component.spec.ts │ │ │ │ └── user-profile.component.ts │ │ │ ├── user.service.spec.ts │ │ │ ├── user.service.ts │ │ │ ├── vuln-form/ │ │ │ │ ├── Vulnerability.ts │ │ │ │ ├── vuln-form.component.html │ │ │ │ ├── vuln-form.component.sass │ │ │ │ ├── vuln-form.component.spec.ts │ │ │ │ └── vuln-form.component.ts │ │ │ └── vulnerability/ │ │ │ ├── vulnerability.component.html │ │ │ ├── vulnerability.component.sass │ │ │ ├── vulnerability.component.spec.ts │ │ │ └── vulnerability.component.ts │ │ ├── assets/ │ │ │ ├── .gitkeep │ │ │ └── theme/ │ │ │ └── button.scss │ │ ├── environments/ │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.html │ │ ├── karma.conf.js │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ ├── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.spec.json │ │ └── tslint.json │ ├── tsconfig.json │ └── tslint.json ├── jest.config.js ├── ormconfig.js ├── package.json ├── src/ │ ├── app.ts │ ├── classes/ │ │ └── Report.ts │ ├── data-source.ts │ ├── entity/ │ │ ├── ApiKey.ts │ │ ├── Assessment.ts │ │ ├── Asset.ts │ │ ├── Config.ts │ │ ├── File.ts │ │ ├── Jira.ts │ │ ├── Organization.ts │ │ ├── ProblemLocation.ts │ │ ├── ReportAudit.ts │ │ ├── Resource.ts │ │ ├── Team.ts │ │ ├── User.ts │ │ ├── VulnDictionary.ts │ │ └── Vulnerability.ts │ ├── enums/ │ │ ├── message-enum.ts │ │ ├── roles-enum.ts │ │ └── status-enum.ts │ ├── init/ │ │ ├── docker-run-exec.ts │ │ └── setEnv.ts │ ├── interfaces/ │ │ ├── jira/ │ │ │ ├── jira-init.interface.ts │ │ │ ├── jira-issue-link.interface.ts │ │ │ ├── jira-issue-priority.interface.ts │ │ │ ├── jira-issue-status.interface.ts │ │ │ ├── jira-issue-type.interface.ts │ │ │ ├── jira-issue.interface.ts │ │ │ ├── jira-project.interface.ts │ │ │ └── jira-result.interface.ts │ │ ├── team-info.interface.ts │ │ └── user-request.interface.ts │ ├── middleware/ │ │ ├── jwt.middleware.ts │ │ └── jwt.spec.ts │ ├── routes/ │ │ ├── api-key.controller.spec.ts │ │ ├── api-key.controller.ts │ │ ├── assessment.controller.spec.ts │ │ ├── assessment.controller.ts │ │ ├── asset.controller.spec.ts │ │ ├── asset.controller.ts │ │ ├── authentication.controller.ts │ │ ├── config.controller.spec.ts │ │ ├── config.controller.ts │ │ ├── file-upload.controller.ts │ │ ├── organization.controller.ts │ │ ├── report-audit.controller.spec.ts │ │ ├── report-audit.controller.ts │ │ ├── team.controller.spec.ts │ │ ├── team.controller.ts │ │ ├── user.controller.spec.ts │ │ ├── user.controller.ts │ │ └── vulnerability.controller.ts │ ├── services/ │ │ ├── email.service.spec.ts │ │ └── email.service.ts │ ├── temp/ │ │ └── empty.ts │ └── utilities/ │ ├── column-mapper.utility.ts │ ├── crypto.utility.spec.ts │ ├── crypto.utility.ts │ ├── file.utility.ts │ ├── jira.utility.spec.ts │ ├── jira.utility.ts │ ├── password.utility.spec.ts │ ├── password.utility.ts │ ├── puppeteer.utility.ts │ └── role.utility.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git/ monitoring/ node_modules/ screenshots/ test/ build/ dist/ vagrant/ docs/ logs/ Dockerfile .npmrc frontend/node_modules/ frontend/dist/ .env ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a bug in Bulwark title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Problem Locations** Did you find this issue while using Bulwark? Supply the URL and payload that caused the issue. Furthermore, supply the relative path and line number of the affected source code. **Steps to Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for Bulwark title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/agents/my-agent.agent.md ================================================ --- # Fill in the fields below to create a basic custom agent for your repository. # The Copilot CLI can be used for local testing: https://gh.io/customagents/cli # To make this agent available, merge this file into the default repository branch. # For format details, see: https://gh.io/customagents/config name: SecBot description: SecBot will look at security issues and perform required remediations. --- # My Agent SecBot will monitor for security issues and address them by performing remediation steps. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'monthly' target-branch: 'develop' - package-ecosystem: 'npm' directory: '/frontend' schedule: interval: 'monthly' target-branch: 'develop' ================================================ FILE: .github/pull_request_template.md ================================================ # Pull Request Thank you for your contribution to the Bulwark. Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) ## Type of change - [ ] A new feature - [ ] A bug fix - [ ] Documentation only changes - [ ] Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) - [ ] A code change that neither fixes a bug nor adds a feature - [ ] A code change that improves performance - [ ] Adding missing tests or correcting existing tests - [ ] Changes that affect the build system or external dependencies - [ ] Changes to our CI configuration files and scripts - [ ] Other changes that don't modify src or test files Add description here ```release-note ``` --- ## Checklist - [ ] My code follows the [contributing](../CONTRIBUTING.md) guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules ================================================ FILE: .github/stale.yml ================================================ daysUntilStale: 60 daysUntilClose: 7 exemptLabels: - confirmed - security staleLabel: stale markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: 'CodeQL' on: push: branches: [master, develop] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 2 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['javascript'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .github/workflows/node.js.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: build on: push: branches: [master] pull_request: branches: [develop] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [12.x, 14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install Packages run: npm install - name: Create Env File run: | touch .env echo JWT_KEY="test" >> .env echo JWT_REFRESH_KEY="test" >> .env - name: Backend Tests run: npm run test:node ================================================ FILE: .gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. #angular /frontend/.angular # compiled output /dist /tmp /out-tsc # Only exists if Bazel was run /bazel-out # dependencies /node_modules # profiling files chrome-profiler-events.json speed-measure-plugin.json # database migrations /src/database/migration/* # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace .vscode/ # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # misc /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log yarn-error.log testem.log /typings .env .env.* # System Files .DS_Store Thumbs.db ================================================ FILE: .jest/setEnvVars.js ================================================ process.env.NODE_ENV = 'test'; process.env.CRYPTO_SECRET = 'test'; process.env.CRYPTO_SALT = 'test'; process.env.JWT_KEY = 'test'; process.env.JWT_REFRESH_KEY = 'test'; process.env.CRYPTO_SALT = 'test'; ================================================ FILE: .prettierignore ================================================ coverage dist frontend/dist node_modules frontend/node_modules .prettierignore Dockerfile CHANGELOG.md # Ignore all HTML files: *.html *.sass .gitignore frontend/.gitignore ================================================ FILE: .prettierrc.json ================================================ { "arrowParens": "always", "bracketSpacing": true, "embeddedLanguageFormatting": "auto", "htmlWhitespaceSensitivity": "css", "insertPragma": false, "jsxBracketSameLine": false, "jsxSingleQuote": false, "printWidth": 80, "proseWrap": "preserve", "quoteProps": "as-needed", "requirePragma": false, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "useTabs": false, "vueIndentScriptAndStyle": false } ================================================ FILE: .versionrc ================================================ { "types": [ {"type": "chore", "section":"Others", "hidden": false}, {"type": "revert", "section":"Reverts", "hidden": false}, {"type": "feat", "section": "Features", "hidden": false}, {"type": "fix", "section": "Bug Fixes", "hidden": false}, {"type": "docs", "section":"Docs", "hidden": false}, {"type": "style", "section":"Styling", "hidden": false}, {"type": "refactor", "section":"Code Refactoring", "hidden": false}, {"type": "perf", "section":"Performance Improvements", "hidden": false}, {"type": "test", "section":"Tests", "hidden": false}, {"type": "build", "section":"Build System", "hidden": false}, {"type": "ci", "section":"CI", "hidden":false} ] } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ## [7.3.0](https://github.com/softrams/bulwark/compare/v7.2.1...v7.3.0) (2021-06-28) ### Features * **asset.controller.ts:** add open vulnerability modal to asset ([1c0b5b0](https://github.com/softrams/bulwark/commit/1c0b5b028a33be1f5225c9417a06635ec5f21726)), closes [#862](https://github.com/softrams/bulwark/issues/862) * **asset.controller.ts:** optimize asset queries. Complete vulnerability table ([7c5729b](https://github.com/softrams/bulwark/commit/7c5729b847f02a5011be5f37f4c6f3758730dca4)), closes [#862](https://github.com/softrams/bulwark/issues/862) ### [7.2.1](https://github.com/softrams/bulwark/compare/v7.2.0...v7.2.1) (2021-06-23) ### Bug Fixes * **app.ts:** fixed missing middleware for update password ([fd2600a](https://github.com/softrams/bulwark/commit/fd2600a77b369c13672d641aff47af9e3190ffe1)), closes [#860](https://github.com/softrams/bulwark/issues/860) ## [7.2.0](https://github.com/softrams/bulwark/compare/v7.1.4...v7.2.0) (2021-06-23) ### Features * **asset.controller.ts:** return number of open vulnerabilities with assets ([8ccbbc7](https://github.com/softrams/bulwark/commit/8ccbbc7c055b3fef92382746a77b2b14a562bf42)), closes [#858](https://github.com/softrams/bulwark/issues/858) ### Docs * **readme.md:** temporarily remove Team section from README ([7b17ee6](https://github.com/softrams/bulwark/commit/7b17ee63f6c330875a78dee4b53002923a6bebd8)) ### [7.1.4](https://github.com/softrams/bulwark/compare/v7.1.3...v7.1.4) (2021-06-21) ### Docs * **readme:** readMe gifs in public AWS repo ([3cef86f](https://github.com/softrams/bulwark/commit/3cef86fbb2de61e563928cedd38607066f36f4f5)), closes [#848](https://github.com/softrams/bulwark/issues/848) ### Build System * **dependabot.yml:** update interval ([a592cd3](https://github.com/softrams/bulwark/commit/a592cd305b8c6be3dd5b5dabeaf34e5139b86627)) ### Others * **dependabot.yml/readme.md:** removed myself from the README. Removed myself from dependabot ([c7676cf](https://github.com/softrams/bulwark/commit/c7676cfce5a17c2eaa83db014aa942eedffd6c59)) * **deps:** bump @ng-select/ng-select from 6.1.0 to 7.0.1 in /frontend ([ee7a3e9](https://github.com/softrams/bulwark/commit/ee7a3e95bcb95a5fe4ff729f1c37ab1e3659ffa1)) * **deps:** bump core-js from 3.12.1 to 3.15.0 in /frontend ([492702d](https://github.com/softrams/bulwark/commit/492702db13772e1a13e6c68b58e83a4a3590b3f8)) * **deps:** bump mime-types from 2.1.30 to 2.1.31 ([1af1aab](https://github.com/softrams/bulwark/commit/1af1aab0136b58a6057a4830d976bb2949e1dee0)) * **deps:** bump nodemailer from 6.6.1 to 6.6.2 ([7e6b9d2](https://github.com/softrams/bulwark/commit/7e6b9d226a8d6d3111517ac4da8840fda6eac124)) * **deps:** bump primeng from 11.4.2 to 12.0.0 in /frontend ([e8318ff](https://github.com/softrams/bulwark/commit/e8318ffa00b6d55ac74fcb57dc411bf035a47f21)) * **deps:** bump puppeteer from 9.1.1 to 10.0.0 ([9dde15b](https://github.com/softrams/bulwark/commit/9dde15b626b799575400cfb4dcabee873721df1c)) * **deps:** bump tslib from 2.2.0 to 2.3.0 in /frontend ([997a635](https://github.com/softrams/bulwark/commit/997a6357f76267dc5f82a503740e43e0bf4761c7)) * **deps:** bump typeorm from 0.2.32 to 0.2.34 ([d6cf805](https://github.com/softrams/bulwark/commit/d6cf805029cd4a3c3202fcc024e42739ad3ecf14)) * **deps-dev:** bump @babel/core from 7.14.3 to 7.14.6 ([44a7ac5](https://github.com/softrams/bulwark/commit/44a7ac56a5a10e38f41ad5258b70b4d63132e717)) * **deps-dev:** bump @babel/preset-env from 7.14.2 to 7.14.5 ([d7af0e2](https://github.com/softrams/bulwark/commit/d7af0e29001224953dfa7042afb8155a8b5bce14)) * **deps-dev:** bump @babel/preset-typescript from 7.13.0 to 7.14.5 ([b34dbde](https://github.com/softrams/bulwark/commit/b34dbde2451ae49f9a0b48e96efad142656c3cf2)) * **deps-dev:** bump @types/jasmine from 3.7.4 to 3.7.7 in /frontend ([205a783](https://github.com/softrams/bulwark/commit/205a78373301e9b4a78949ec9666a7214d3fe4fc)) * **deps-dev:** bump @types/node from 15.6.1 to 15.12.4 in /frontend ([987b2c7](https://github.com/softrams/bulwark/commit/987b2c72843f0fcf8e7e15c2d4f07907e64dafbb)) * **deps-dev:** bump babel-jest from 26.6.3 to 27.0.2 ([ec3fbbf](https://github.com/softrams/bulwark/commit/ec3fbbf92cde2f45a348312518f534e35cef0db9)) * **deps-dev:** bump highlight.js from 10.7.2 to 11.0.1 ([19d901c](https://github.com/softrams/bulwark/commit/19d901c43da8cea6a9690d7357f5bbeb5961cc20)) * **deps-dev:** bump jest from 26.6.3 to 27.0.4 ([c7a96a9](https://github.com/softrams/bulwark/commit/c7a96a986b38b7034c2766583e71728ebb2cd634)) * **deps-dev:** bump karma from 6.3.2 to 6.3.4 in /frontend ([0eb8f1a](https://github.com/softrams/bulwark/commit/0eb8f1ad69436cca44cf939207e27bf5735ccddd)) * **deps-dev:** bump prettier from 2.3.0 to 2.3.1 ([8430f66](https://github.com/softrams/bulwark/commit/8430f66c411a1271153781d242821de23deade38)) * **deps-dev:** bump typescript from 4.2.4 to 4.3.4 ([c5c571c](https://github.com/softrams/bulwark/commit/c5c571c78a25eae6698cba047471deace06a4b67)) * **ng update @angular/cdk:** ng update @angular/cdk ([03cfc4b](https://github.com/softrams/bulwark/commit/03cfc4b8786f61e84648fbe1594136f7b42cfe77)) * **ng update @angular/cli:** ng update @angular/cli ([075711d](https://github.com/softrams/bulwark/commit/075711deff26e44659facaeb3b12578d33043b36)) * **npm audit fix:** npm audit fix ([8d6774f](https://github.com/softrams/bulwark/commit/8d6774f53ef68546445448001d15073d09423c26)) * **readme.md:** fix conflict ([a05147e](https://github.com/softrams/bulwark/commit/a05147e9c93dce14db0f301a35bbe73a5f04d554)) * **unit tests:** fixed Jira and Password unit tests ([887e6a7](https://github.com/softrams/bulwark/commit/887e6a7d564f54cbe0d531894cf72f4ddcf88603)) * **update angular/core:** update angular/core ([e6cb7ff](https://github.com/softrams/bulwark/commit/e6cb7ffecec61b647a6939e74387ff5870c62966)) ### [7.1.3](https://github.com/softrams/bulwark/compare/v7.1.2...v7.1.3) (2021-06-04) ### [7.1.2](https://github.com/softrams/bulwark/compare/v7.1.1...v7.1.2) (2021-05-27) ### Others * **package-lock.json:** update package-lock.json to force docker build ([b1904c4](https://github.com/softrams/bulwark/commit/b1904c440009da267a50ee689045e127869844ee)) ### [7.1.1](https://github.com/softrams/bulwark/compare/v7.1.0...v7.1.1) (2021-05-26) ### Bug Fixes * **dockerfile:** update `NODE_VERSION` to 14.9.0 ([3ba025f](https://github.com/softrams/bulwark/commit/3ba025f13abccf522e25782cbeeef64ee3ca507d)) ## [7.1.0](https://github.com/softrams/bulwark/compare/v7.0.11...v7.1.0) (2021-05-25) ### Features * **assessments.component.html:** add `sortField` and `sortOrder` to assessments table ([f3dbd42](https://github.com/softrams/bulwark/commit/f3dbd427d33a11ebd413c5a11129caa8bea7cce2)) ### Build System * **node.js.yml:** remove support for Node version 10.x ([9938b24](https://github.com/softrams/bulwark/commit/9938b2463d409412bb85cbfdbe62d3e8ba2df31e)) ### Others * **deps:** bump concurrently from 6.0.2 to 6.1.0 ([817e4e9](https://github.com/softrams/bulwark/commit/817e4e97c07756d0c392867161bcf6946af4b96d)) * **deps:** bump concurrently from 6.1.0 to 6.2.0 ([826b576](https://github.com/softrams/bulwark/commit/826b576f2493221f0f8b2ab0e16bea016661bbee)) * **deps:** bump core-js from 3.11.0 to 3.12.1 in /frontend ([8102fd6](https://github.com/softrams/bulwark/commit/8102fd629c391360a7a933fed10b2dcea106510f)) * **deps:** bump dotenv from 8.2.0 to 10.0.0 ([51d9c8f](https://github.com/softrams/bulwark/commit/51d9c8fa251dc36e5ade49276730a6edbc8cb648)) * **deps:** bump helmet from 4.5.0 to 4.6.0 ([64c2fb3](https://github.com/softrams/bulwark/commit/64c2fb37377b1db74c900fbbddcfbe0e37b4cc08)) * **deps:** bump ngx-markdown from 11.1.3 to 12.0.1 in /frontend ([a1bf0e4](https://github.com/softrams/bulwark/commit/a1bf0e459e6f1f44df15ae9af4400e83dfc09825)) * **deps:** bump nodemailer from 6.5.0 to 6.6.1 ([536c513](https://github.com/softrams/bulwark/commit/536c513f9dde65cd6a7a63656b1cd28864f8972d)) * **deps:** bump primeng from 11.4.0 to 11.4.2 in /frontend ([868bd72](https://github.com/softrams/bulwark/commit/868bd727fba29181e55612caa12278d967a49b49)) * **deps:** bump puppeteer from 9.0.0 to 9.1.1 ([ffd5426](https://github.com/softrams/bulwark/commit/ffd5426a9d0970aef00292e00069a468802e7165)) * **deps:** bump ts-node from 9.1.1 to 10.0.0 ([0e471a9](https://github.com/softrams/bulwark/commit/0e471a9c79326037e7fa48755e0c2a3df04266cd)) * **deps-dev:** bump @babel/core from 7.13.16 to 7.14.3 ([393f0c6](https://github.com/softrams/bulwark/commit/393f0c6a30cd0e4146b4ccd0d7c318bd15e2a0fa)) * **deps-dev:** bump @babel/preset-env from 7.13.15 to 7.14.2 ([4ddd3bb](https://github.com/softrams/bulwark/commit/4ddd3bb4927223b1d5015e81e3860b005d503d42)) * **deps-dev:** bump @commitlint/cli from 12.1.1 to 12.1.4 ([1af5966](https://github.com/softrams/bulwark/commit/1af59660bf9c686ae18b877b66ff8cc9dccd74e9)) * **deps-dev:** bump @commitlint/config-conventional ([159f930](https://github.com/softrams/bulwark/commit/159f9303bdcf6ab53a7921d22a011b8d64d71c90)) * **deps-dev:** bump @types/jasmine from 3.6.9 to 3.7.4 in /frontend ([8b1b4da](https://github.com/softrams/bulwark/commit/8b1b4daa1910b1f322c3624bee7d74fc9d0c17cf)) * **deps-dev:** bump @types/jasminewd2 from 2.0.8 to 2.0.9 in /frontend ([50d58bf](https://github.com/softrams/bulwark/commit/50d58bf9d10886fee6a215c647280db96d987420)) * **deps-dev:** bump @types/jest from 26.0.22 to 26.0.23 ([778ee66](https://github.com/softrams/bulwark/commit/778ee66f3b655e96b9c83a197b8806d49a956c16)) * **deps-dev:** bump @types/node from 14.14.41 to 15.6.1 in /frontend ([480221a](https://github.com/softrams/bulwark/commit/480221a7d6f40ad6ca8dab5449f578cfd81217a8)) * **deps-dev:** bump codelyzer from 6.0.1 to 6.0.2 in /frontend ([da6f92e](https://github.com/softrams/bulwark/commit/da6f92ea07ed06b36996f98666653842525cf5cc)) * **deps-dev:** bump karma-jasmine-html-reporter in /frontend ([2073d3a](https://github.com/softrams/bulwark/commit/2073d3a0a7e41495c9710fe82935fe99071ef657)) * **deps-dev:** bump lint-staged from 10.5.4 to 11.0.0 ([87b5b48](https://github.com/softrams/bulwark/commit/87b5b48854c3ef7495c46558ba328f03e8ecc132)) * **deps-dev:** bump prettier from 2.2.1 to 2.3.0 ([36fe885](https://github.com/softrams/bulwark/commit/36fe885b5d722ebf6c26dce07928a950cb4e824a)) * **deps-dev:** bump standard-version from 9.2.0 to 9.3.0 ([50ba4aa](https://github.com/softrams/bulwark/commit/50ba4aa3f459f6b2a11ec137d1e875dce2f4b72b)) * **deps-dev:** bump ts-jest from 26.5.5 to 26.5.6 ([4214046](https://github.com/softrams/bulwark/commit/4214046b1d43f59502e587d71f9c9708a9a5f49a)) * **deps-dev:** bump ts-node from 9.1.1 to 10.0.0 in /frontend ([e1af4c3](https://github.com/softrams/bulwark/commit/e1af4c3d096e661c561a860ac984afd1edba4754)) * **frontend/package.json:** update Angular to v12 ([be6ac28](https://github.com/softrams/bulwark/commit/be6ac2829d1b2591a9a33aed4cddc1895d4ecee4)) * **package-lock.json:** npm audit fix ([c401f46](https://github.com/softrams/bulwark/commit/c401f46cda9e7a8c8bbd41a38c2d54bd391465d8)) * **package-lock.json:** update lockfileversion to 2 ([63c2460](https://github.com/softrams/bulwark/commit/63c24607d9183200621d583ea56c2946b2773255)) * **update @angular/cdk:** update @angular/cdk ([e36a9b6](https://github.com/softrams/bulwark/commit/e36a9b632731959b8d76576346e9ce00d5660cdd)) * **update @angular/cli:** update @angular/cli ([2a1cbde](https://github.com/softrams/bulwark/commit/2a1cbdee1fdaad04df643f604fb1f7da82ddb977)) * **update @angular/core:** update @angular/core ([e64a3ba](https://github.com/softrams/bulwark/commit/e64a3ba6cff98cd5594f5c7a4ab6de009ea31180)) ### [7.0.11](https://github.com/softrams/bulwark/compare/v7.0.10...v7.0.11) (2021-05-10) ### Bug Fixes * **multiple api's:** add additional ACL checks ([2cdc01a](https://github.com/softrams/bulwark/commit/2cdc01abaf6d499951407105d56883956c644e28)) * **role.utility.ts:** updated role-utility.ts ([f0fc0c5](https://github.com/softrams/bulwark/commit/f0fc0c57e5f0d13d287f7305900448a8bd80f6fe)) ### Others * **deps:** bump core-js from 3.10.2 to 3.11.0 in /frontend ([90aa392](https://github.com/softrams/bulwark/commit/90aa392655accf6d6212fc8bbadbd852518d773b)) * **deps:** bump primeng from 11.3.2 to 11.4.0 in /frontend ([211c294](https://github.com/softrams/bulwark/commit/211c29491912cf95f8c71494f2be77b5397ac498)) * **deps:** bump puppeteer from 8.0.0 to 9.0.0 ([9dbe9d2](https://github.com/softrams/bulwark/commit/9dbe9d299fb613690b078be01defc290ae9162b6)) * **deps-dev:** bump @babel/core from 7.13.15 to 7.13.16 ([08fa5a6](https://github.com/softrams/bulwark/commit/08fa5a647322f4dbfb599ecc2ea91f7a0de2e028)) * **frontend/package.json:** ng update @angular/cli ([58ad735](https://github.com/softrams/bulwark/commit/58ad735626521f4aa1480649a1a155982082dd3a)) * **frontend/package.json:** update @angular/cdk ([51fe3c1](https://github.com/softrams/bulwark/commit/51fe3c16d111d9dd3d960ae6048fe4096f575782)) * **frontend/package.json:** update @angular/cli ([b3b0a18](https://github.com/softrams/bulwark/commit/b3b0a18cf41b4ea5ceb14710c6e21f149119f3b6)) * **frontend/package.json:** update @angular/core ([8f8a247](https://github.com/softrams/bulwark/commit/8f8a24778a158deae7d8b936f815569bb3ddbc1a)) * **ng update @angular/cdk:** ng update @angular/cdk ([d960688](https://github.com/softrams/bulwark/commit/d9606880878f75aa61ce6d08a426b8f12ef37b65)) * **package.json:** ng update @angular/core ([431b10e](https://github.com/softrams/bulwark/commit/431b10ec7521e92c7f4aa4358816edce89b31da7)) * **package.json:** npm audit fix ([6a9f8b1](https://github.com/softrams/bulwark/commit/6a9f8b1b8fef2bb846e3dcd72e12080edee76270)) ### [7.0.10](https://github.com/softrams/bulwark/compare/v7.0.9...v7.0.10) (2021-04-23) ### Bug Fixes * **chart.js:** reverting chart.js to v2 ([05b7704](https://github.com/softrams/bulwark/commit/05b77040bee13cd7040a81cacb02f949b9b230a0)) ### [7.0.9](https://github.com/softrams/bulwark/compare/v7.0.8...v7.0.9) (2021-04-23) ### Bug Fixes * **frontend/package.json:** update angular CLI ([3107c38](https://github.com/softrams/bulwark/commit/3107c389226dac0ee7037328d4849a91d56d70e9)) * **password.utility.ts:** update password utility and user.controller for readability ([d6cdebb](https://github.com/softrams/bulwark/commit/d6cdebbe4958c85946f653779d7b0eab13e18b6f)), closes [#753](https://github.com/softrams/bulwark/issues/753) * **user.controller.ts:** fixed failing unit tests ([ff08636](https://github.com/softrams/bulwark/commit/ff0863640765c119d8064724359a813069b3b109)), closes [#753](https://github.com/softrams/bulwark/issues/753) ### Others * **deps:** bump @angular-devkit/build-angular in /frontend ([3543a48](https://github.com/softrams/bulwark/commit/3543a48c0026c35cab27188ed6fd457e9983d9d4)) * **deps:** bump @angular/cdk from 11.2.7 to 11.2.8 in /frontend ([414e270](https://github.com/softrams/bulwark/commit/414e27021febd4befe7e3133cd5e93b931b31f67)) * **deps:** bump @angular/router from 11.2.8 to 11.2.9 in /frontend ([65b900e](https://github.com/softrams/bulwark/commit/65b900ef4f7db31e9ec18fa1a89579e29b4e687c)) * **deps:** bump chart.js from 3.0.2 to 3.1.0 in /frontend ([c1c9a97](https://github.com/softrams/bulwark/commit/c1c9a97dfe5761da07f366bfbafdc420bcab473b)) * **deps:** bump chart.js from 3.1.0 to 3.1.1 in /frontend ([346cb8b](https://github.com/softrams/bulwark/commit/346cb8b1b0912c826f76d5fa2d277e3856280979)) * **deps:** bump concurrently from 6.0.0 to 6.0.1 ([217bcfe](https://github.com/softrams/bulwark/commit/217bcfec490913dacf44fc045815020fdcb4ab98)) * **deps:** bump concurrently from 6.0.1 to 6.0.2 ([337ac3a](https://github.com/softrams/bulwark/commit/337ac3aa8d515703f817f996393f92ca29334333)) * **deps:** bump core-js from 3.10.0 to 3.10.1 in /frontend ([8a321eb](https://github.com/softrams/bulwark/commit/8a321ebeed19ded4609a0ee816b9e173208b468f)) * **deps:** bump core-js from 3.10.1 to 3.10.2 in /frontend ([7e99aec](https://github.com/softrams/bulwark/commit/7e99aec0503dfeec4f0f449055265ca30c1ba65c)) * **deps:** bump helmet from 4.4.1 to 4.5.0 ([12275da](https://github.com/softrams/bulwark/commit/12275da6475a19368b0d9444446e0f9594d6bafd)) * **deps:** bump ngx-markdown from 11.1.2 to 11.1.3 in /frontend ([deec7bf](https://github.com/softrams/bulwark/commit/deec7bf0abf33889382511cbf21c799f7e3c0e66)) * **deps:** bump primeng from 11.3.1 to 11.3.2 in /frontend ([b296584](https://github.com/softrams/bulwark/commit/b296584376a6cd3c4fd1065d723c320f2a27f069)) * **deps:** bump tslib from 2.1.0 to 2.2.0 in /frontend ([090415a](https://github.com/softrams/bulwark/commit/090415a6be66454e239469aec7cdb30ca361462a)) * **deps-dev:** bump @babel/core from 7.13.13 to 7.13.15 ([4d2430d](https://github.com/softrams/bulwark/commit/4d2430dfd4225632179851f1b9f9e19619a57b70)) * **deps-dev:** bump @babel/preset-env from 7.13.12 to 7.13.15 ([ca01c10](https://github.com/softrams/bulwark/commit/ca01c102cac3176e608b559b6cd5c2ad3fa077c2)) * **deps-dev:** bump @types/node from 14.14.37 to 14.14.41 in /frontend ([2ae9804](https://github.com/softrams/bulwark/commit/2ae98040ecc6fc6b09bb6e81ac3448188c595a9f)) * **deps-dev:** bump jasmine-spec-reporter in /frontend ([2da5194](https://github.com/softrams/bulwark/commit/2da51946fcf47a7f8a50dd884603ce50ec7ec59b)) * **deps-dev:** bump standard-version from 9.1.1 to 9.2.0 ([b1623d9](https://github.com/softrams/bulwark/commit/b1623d951b713eab4f82e086a3518c04e64367b0)) * **deps-dev:** bump ts-jest from 26.5.4 to 26.5.5 ([371a25b](https://github.com/softrams/bulwark/commit/371a25bc304577dbad5e8cea0b09df7da25623f1)) * **deps-dev:** bump typescript from 4.2.3 to 4.2.4 ([a0bf85f](https://github.com/softrams/bulwark/commit/a0bf85fe62c951dca8d45099c5818c33be622fbc)) * **frontend/package.json:** update @angular/cdk ([dd5aab2](https://github.com/softrams/bulwark/commit/dd5aab2904dc4b9be7bfb138fced39ebdd54c8f3)) * **frontend/package.json:** update @angular/cli ([1ecd2ce](https://github.com/softrams/bulwark/commit/1ecd2ce752d3e0bd5baa8d5dddd9b2e53c89e707)) * **frontend/package.json:** update @angular/core ([9052f8a](https://github.com/softrams/bulwark/commit/9052f8a084299f3ff81954ba3b931a03dc8e183d)) * **frontend/package.json:** update global angular version ([9faf260](https://github.com/softrams/bulwark/commit/9faf260f296f49cea163893ce9857cc665f0fbfb)) ### [7.0.8](https://github.com/softrams/bulwark/compare/v7.0.7...v7.0.8) (2021-04-13) ### Bug Fixes * **chart.js:** rever chart.js to v2 ([0c59004](https://github.com/softrams/bulwark/commit/0c5900431d92d4a71ff304749af7e84394674dd1)) ### [7.0.7](https://github.com/softrams/bulwark/compare/v7.0.6...v7.0.7) (2021-04-06) ### Bug Fixes * **frontend/package.json:** downgrade Typescript. Downgrade Angular ([bd96650](https://github.com/softrams/bulwark/commit/bd96650c4278800f54443f24ec3e4ff597be6cb7)) ### Others * **deps:** bump @angular-devkit/build-angular in /frontend ([b6b577f](https://github.com/softrams/bulwark/commit/b6b577f16cccab8d8cf1a5a4229bdfe954a9184d)) * **deps:** bump @angular-devkit/build-angular in /frontend ([72fa45b](https://github.com/softrams/bulwark/commit/72fa45b18a5cc39023cb1755967bd012d11be457)) * **deps:** bump @angular-devkit/build-angular in /frontend ([4a94d44](https://github.com/softrams/bulwark/commit/4a94d4423ecd60953639756106f387866c769434)) * **deps:** bump @angular/animations from 11.0.0 to 11.0.9 in /frontend ([9fbd8e5](https://github.com/softrams/bulwark/commit/9fbd8e53753f8cb0c2ac2275494b20b840c618bd)) * **deps:** bump @angular/cdk from 11.2.4 to 11.2.5 in /frontend ([2a80c7c](https://github.com/softrams/bulwark/commit/2a80c7cfa7c18178d2c938ea0d0bc95ebe4cf23d)) * **deps:** bump @angular/cdk from 11.2.5 to 11.2.6 in /frontend ([2aac976](https://github.com/softrams/bulwark/commit/2aac97649670c910782c4324ac0cfab30bc6cf79)) * **deps:** bump @angular/cli from 11.2.4 to 11.2.5 in /frontend ([ec8d363](https://github.com/softrams/bulwark/commit/ec8d363ffef230deec2640273da8624e2a1afaa8)) * **deps:** bump @angular/cli from 11.2.5 to 11.2.6 in /frontend ([2566862](https://github.com/softrams/bulwark/commit/25668626ec64ff599ae0414c2aeb7203457f04fb)) * **deps:** bump @angular/cli from 11.2.6 to 11.2.7 in /frontend ([3b492ba](https://github.com/softrams/bulwark/commit/3b492ba39302894bfa0120bc0cfb1f99861362ac)) * **deps:** bump @angular/common from 11.0.0 to 11.0.9 in /frontend ([0d79621](https://github.com/softrams/bulwark/commit/0d79621386d914371d0fb416785ef3f07983a12f)) * **deps:** bump @angular/compiler from 11.0.0 to 11.2.8 in /frontend ([7c5ad50](https://github.com/softrams/bulwark/commit/7c5ad5021efc04fa2d2a6b0784bafa290e8f108a)) * **deps:** bump @angular/core from 11.0.0 to 11.2.5 in /frontend ([025d6fd](https://github.com/softrams/bulwark/commit/025d6fd82202b3cd2fd96433f2bdfb29d64d243a)) * **deps:** bump @angular/forms from 11.0.0 to 11.2.7 in /frontend ([5d79f0f](https://github.com/softrams/bulwark/commit/5d79f0fd3991c6c56c5899587728ac97e148a376)) * **deps:** bump @angular/platform-browser in /frontend ([5fe50eb](https://github.com/softrams/bulwark/commit/5fe50eb8bf30340ffaced73169378afeb8b0ab15)) * **deps:** bump @angular/platform-browser in /frontend ([9d1d6dd](https://github.com/softrams/bulwark/commit/9d1d6dd452f5b03e30d42c92fdf3cab3b1f68798)) * **deps:** bump @angular/platform-browser-dynamic in /frontend ([036932f](https://github.com/softrams/bulwark/commit/036932f15f0c8624e1d9a7d1da3b7f523aeac175)) * **deps:** bump @angular/router from 11.0.0 to 11.2.7 in /frontend ([30d45ca](https://github.com/softrams/bulwark/commit/30d45caf0b72e8ecf98ecbcb4753ca669af9ac4f)) * **deps:** bump @angular/router from 11.2.7 to 11.2.8 in /frontend ([03c65e7](https://github.com/softrams/bulwark/commit/03c65e7ddf897f40663422c2e3d78f98e59c7e7d)) * **deps:** bump chart.js from 2.9.4 to 3.0.2 in /frontend ([79e8fa8](https://github.com/softrams/bulwark/commit/79e8fa81ff9a876dd7b20a5f0c56254340c4ee10)) * **deps:** bump core-js from 3.9.1 to 3.10.0 in /frontend ([67f34d9](https://github.com/softrams/bulwark/commit/67f34d9c28f6595409f9d3e171790136256d7c8e)) * **deps:** bump mime-types from 2.1.29 to 2.1.30 ([d0545a7](https://github.com/softrams/bulwark/commit/d0545a75e6e4cad7ee80ff0676760ed045353006)) * **deps:** bump primeng from 11.2.3 to 11.3.1 in /frontend ([f1e1563](https://github.com/softrams/bulwark/commit/f1e15633ea51dd83b366d5f922f3d24b5c66f4b8)) * **deps:** bump rxjs from 6.6.6 to 6.6.7 in /frontend ([1d9a659](https://github.com/softrams/bulwark/commit/1d9a659bd39c78fa2c3a689653febfeb71f95630)) * **deps:** bump typeorm from 0.2.31 to 0.2.32 ([a95c62e](https://github.com/softrams/bulwark/commit/a95c62e7c308bf165dad94eaae515a3215b4cd54)) * **deps:** bump typescript from 4.0.5 to 4.2.3 in /frontend ([c0c6b0e](https://github.com/softrams/bulwark/commit/c0c6b0e653d8788fc8d228b03c65127fc221806e)) * **deps:** bump zone.js from 0.10.3 to 0.11.4 in /frontend ([516ea44](https://github.com/softrams/bulwark/commit/516ea445faf8d0c0cf4dd8e3067b7abdf52eddcd)) * **deps-dev:** bump @angular/language-service in /frontend ([b0d9084](https://github.com/softrams/bulwark/commit/b0d90849663f71da7b11f4c88f8a26305deaaaf0)) * **deps-dev:** bump @angular/language-service in /frontend ([5b28665](https://github.com/softrams/bulwark/commit/5b286652a913b9a49f57f73a35a8dc18f553117b)) * **deps-dev:** bump @angular/language-service in /frontend ([83d258c](https://github.com/softrams/bulwark/commit/83d258ceb6c599072e798d4ff3f21254b0019f45)) * **deps-dev:** bump @babel/core from 7.13.10 to 7.13.13 ([d00445a](https://github.com/softrams/bulwark/commit/d00445a43fe181befdc9109eb34d7176b9b6a246)) * **deps-dev:** bump @babel/preset-env from 7.13.10 to 7.13.12 ([42d18a1](https://github.com/softrams/bulwark/commit/42d18a19f18733d277c5af5f5ac72746a3cca730)) * **deps-dev:** bump @commitlint/cli from 12.0.1 to 12.1.1 ([d983fe1](https://github.com/softrams/bulwark/commit/d983fe1fb15917481c5ec0f5fa908241d0505075)) * **deps-dev:** bump @commitlint/config-conventional ([f4ed18a](https://github.com/softrams/bulwark/commit/f4ed18a6127180ee0353543113d5e9b40310c42d)) * **deps-dev:** bump @types/jasmine from 3.6.6 to 3.6.7 in /frontend ([e43dbf0](https://github.com/softrams/bulwark/commit/e43dbf0689c6de24d4876edb01c576e80e5ebe53)) * **deps-dev:** bump @types/jasmine from 3.6.7 to 3.6.9 in /frontend ([2137158](https://github.com/softrams/bulwark/commit/21371582d0f8e4812a7084c58fb67c5b3b30765c)) * **deps-dev:** bump @types/jest from 26.0.20 to 26.0.21 ([44fba40](https://github.com/softrams/bulwark/commit/44fba40f43aaa55c682a066a31a4a6423c94fc04)) * **deps-dev:** bump @types/jest from 26.0.21 to 26.0.22 ([f6eb459](https://github.com/softrams/bulwark/commit/f6eb4597579c45ce7475631aee493bcd95de7ec9)) * **deps-dev:** bump @types/node from 14.14.33 to 14.14.34 in /frontend ([eb11d60](https://github.com/softrams/bulwark/commit/eb11d60b459a72f0925f47e2394ded1bbfa12dd3)) * **deps-dev:** bump @types/node from 14.14.34 to 14.14.35 in /frontend ([7a517b1](https://github.com/softrams/bulwark/commit/7a517b1566c77cc09491b94740b7b6b3757656fa)) * **deps-dev:** bump @types/node from 14.14.35 to 14.14.37 in /frontend ([ad90266](https://github.com/softrams/bulwark/commit/ad90266b452e5c4ebdb053f5f6cd920d00e8145c)) * **deps-dev:** bump highlight.js from 10.6.0 to 10.7.1 ([5e4501f](https://github.com/softrams/bulwark/commit/5e4501f463b6291f6fc7dd208e7a906df88366e7)) * **deps-dev:** bump highlight.js from 10.7.1 to 10.7.2 ([bffc257](https://github.com/softrams/bulwark/commit/bffc257d3af1d7af102fa8e65719495dbf4a28a3)) * **deps-dev:** bump husky from 5.1.3 to 5.2.0 ([0227507](https://github.com/softrams/bulwark/commit/02275075d135c05d91a183321bdcf97c7afb17eb)) * **deps-dev:** bump husky from 5.2.0 to 6.0.0 ([672a3e1](https://github.com/softrams/bulwark/commit/672a3e1f9dad542d588c12bd6c2d5928f78e1432)) * **deps-dev:** bump jasmine-core from 3.6.0 to 3.7.1 in /frontend ([8128fac](https://github.com/softrams/bulwark/commit/8128fac61dad7cbbf2498711c8d84d6250a4557b)) * **deps-dev:** bump karma from 6.1.1 to 6.2.0 in /frontend ([064857a](https://github.com/softrams/bulwark/commit/064857a24854b4aba7321cfa562df59c740071d5)) * **deps-dev:** bump karma from 6.2.0 to 6.3.1 in /frontend ([4a537cd](https://github.com/softrams/bulwark/commit/4a537cd4fa7b83ba8ddb7e1cd908a242cf7db46f)) * **deps-dev:** bump karma from 6.3.1 to 6.3.2 in /frontend ([8c9445b](https://github.com/softrams/bulwark/commit/8c9445b60d2d6b1c54a10b08101662d6f7f553ce)) * **deps-dev:** bump ts-jest from 26.5.3 to 26.5.4 ([aad407d](https://github.com/softrams/bulwark/commit/aad407d8fe5c64531b5fa8c5dd879e8db6df329a)) * **frontend/package.json:** angular forced update ([898b100](https://github.com/softrams/bulwark/commit/898b1005921ac4b0b20edb5175f9b4459da17cac)) * **frontend/package.json:** update @angular/cdk ([a01f024](https://github.com/softrams/bulwark/commit/a01f0247a1859af656815ab164845ad736abda9b)) * **package.json:** update y18n to fix security vulnerability ([e452c4c](https://github.com/softrams/bulwark/commit/e452c4c547ca303ac3eba57649f77aceb8c88068)) ### [7.0.6](https://github.com/softrams/bulwark/compare/v7.0.5...v7.0.6) (2021-03-11) ### Bug Fixes * **puppeteer.utility.ts:** fix format error with Puppeteer ([daaf8d2](https://github.com/softrams/bulwark/commit/daaf8d2deb76035e118faaafe480f5b9d16b5238)), closes [#629](https://github.com/softrams/bulwark/issues/629) ### Others * **deps:** bump @angular-devkit/build-angular in /frontend ([c88e50a](https://github.com/softrams/bulwark/commit/c88e50af2002e99e86395674496db761a6430138)) * **deps:** bump @angular-devkit/build-angular in /frontend ([3dfa6e0](https://github.com/softrams/bulwark/commit/3dfa6e035a549cb31e63d84bbcb9e893c488f1d5)) * **deps:** bump @angular/cdk from 11.2.1 to 11.2.2 in /frontend ([a0a3c94](https://github.com/softrams/bulwark/commit/a0a3c94eed9bdfea8743697710d9354f2a7cd5dc)) * **deps:** bump @angular/cdk from 11.2.2 to 11.2.3 in /frontend ([a9337e1](https://github.com/softrams/bulwark/commit/a9337e1c10fa87e8f61e91b6e0f0abb619d44197)) * **deps:** bump @angular/cli from 11.2.1 to 11.2.2 in /frontend ([897c052](https://github.com/softrams/bulwark/commit/897c052470fbfc382854e82c750887094aef50f0)) * **deps:** bump @angular/cli from 11.2.2 to 11.2.3 in /frontend ([fa6f145](https://github.com/softrams/bulwark/commit/fa6f145953930de713512654a64fcfc54963ccec)) * **deps:** bump bcrypt from 5.0.0 to 5.0.1 ([0c7f08e](https://github.com/softrams/bulwark/commit/0c7f08e8bb54e26f5460a41199871fe2a0cb8ca4)) * **deps:** bump core-js from 3.9.0 to 3.9.1 in /frontend ([4d474b7](https://github.com/softrams/bulwark/commit/4d474b7e2186a0f7ed721f6f7d8400f22839560e)) * **deps:** bump jira2md from 2.0.5 to 2.1.0 ([cff79ba](https://github.com/softrams/bulwark/commit/cff79bad62b51b836d9e4d2401ee3912e7077f32)) * **deps:** bump ngx-markdown from 11.1.0 to 11.1.1 in /frontend ([c7160dc](https://github.com/softrams/bulwark/commit/c7160dc0b17d7de552f18a5f091a3f21a8fe8614)) * **deps:** bump nodemailer from 6.4.18 to 6.5.0 ([58e85a8](https://github.com/softrams/bulwark/commit/58e85a83da3fcc498311d551cd6dc31faca9f307)) * **deps:** bump puppeteer from 5.5.0 to 8.0.0 ([a8a60d6](https://github.com/softrams/bulwark/commit/a8a60d66d44be281cbfbb6baf186d7360b01f8ac)) * **deps:** bump rxjs from 6.6.3 to 6.6.6 in /frontend ([9278b10](https://github.com/softrams/bulwark/commit/9278b101c4b44b278a3e015485b20fa7ddd68b67)) * **deps-dev:** bump @angular/language-service in /frontend ([df4439d](https://github.com/softrams/bulwark/commit/df4439df1dee6b7514db04551f8449115f0ed03e)) * **deps-dev:** bump @angular/language-service in /frontend ([9b57502](https://github.com/softrams/bulwark/commit/9b57502039953995af5ce681aa3b54de41fee51d)) * **deps-dev:** bump @babel/core from 7.12.17 to 7.13.8 ([b748325](https://github.com/softrams/bulwark/commit/b7483259827b2ac61571841cbbf51cd171cfad95)) * **deps-dev:** bump @babel/preset-env from 7.12.17 to 7.13.8 ([2537815](https://github.com/softrams/bulwark/commit/2537815a5a881f218c7a02923e9cde6a1d8c675b)) * **deps-dev:** bump @babel/preset-env from 7.13.8 to 7.13.9 ([fa8c224](https://github.com/softrams/bulwark/commit/fa8c2244dcb419de8cf9f380d25000d88778587d)) * **deps-dev:** bump @babel/preset-typescript from 7.12.17 to 7.13.0 ([ff119ee](https://github.com/softrams/bulwark/commit/ff119eefe5cefdacc918995f3e2f132820bdda96)) * **deps-dev:** bump @commitlint/cli from 11.0.0 to 12.0.1 ([e27f94d](https://github.com/softrams/bulwark/commit/e27f94d5a46911009511a18cdee24939aa617043)) * **deps-dev:** bump @commitlint/config-conventional ([6a5dfff](https://github.com/softrams/bulwark/commit/6a5dffff69e07eb6448963f431aa18a31c03656f)) * **deps-dev:** bump @types/jasmine from 3.6.4 to 3.6.6 in /frontend ([74caec6](https://github.com/softrams/bulwark/commit/74caec62fcec3032a43d3d41464de691934467c5)) * **deps-dev:** bump @types/node from 14.14.31 to 14.14.32 in /frontend ([bec5d54](https://github.com/softrams/bulwark/commit/bec5d54d346255624ab3230564972dd28fe7f032)) * **deps-dev:** bump husky from 5.1.0 to 5.1.2 ([f1f788d](https://github.com/softrams/bulwark/commit/f1f788d22df97cc5fb88a0885a5d5241e0379be4)) * **deps-dev:** bump husky from 5.1.2 to 5.1.3 ([c70aba1](https://github.com/softrams/bulwark/commit/c70aba1d96c51724a983fa463b2378e721513673)) * **deps-dev:** bump ts-jest from 26.5.1 to 26.5.2 ([acbdebf](https://github.com/softrams/bulwark/commit/acbdebf3613a1eeb25c9e32c56085b74c2c3e9b4)) * **deps-dev:** bump ts-jest from 26.5.2 to 26.5.3 ([e781e86](https://github.com/softrams/bulwark/commit/e781e86ce1b5067a43d2befd0941e77fe5aa4cb1)) * **deps-dev:** bump typescript from 4.1.5 to 4.2.2 ([8667aff](https://github.com/softrams/bulwark/commit/8667aff32502288037d054e9a18cead6f353ea12)) * **deps-dev:** bump typescript from 4.2.2 to 4.2.3 ([20cb44c](https://github.com/softrams/bulwark/commit/20cb44c188967bc96ba16c4fb8aa87abd845b9cd)) * **package.json:** fixing conflicts ([3555d42](https://github.com/softrams/bulwark/commit/3555d42965c32956d930ccf9a78afa56c72fbbcf)) ### [7.0.5](https://github.com/softrams/bulwark/compare/v7.0.4...v7.0.5) (2021-02-25) ### Bug Fixes * **report component:** fix pdf bug ([32f49cf](https://github.com/softrams/bulwark/commit/32f49cf954676990bbe20375effd7fdc852260da)), closes [#630](https://github.com/softrams/bulwark/issues/630) ### Others * **deps:** bump @angular-devkit/build-angular in /frontend ([d0d3aa6](https://github.com/softrams/bulwark/commit/d0d3aa6222b3f42270643b507a2f84b46d6348d8)) * **deps:** bump @angular-devkit/build-angular in /frontend ([06a7952](https://github.com/softrams/bulwark/commit/06a7952a6c6ded4d252d9eae4e48ca9f200d68ce)) * **deps:** bump @angular/cdk from 11.1.2 to 11.2.0 in /frontend ([50f4127](https://github.com/softrams/bulwark/commit/50f41271566127f54ce9bcc76044a1078e7823c0)) * **deps:** bump @angular/cdk from 11.2.0 to 11.2.1 in /frontend ([9183a72](https://github.com/softrams/bulwark/commit/9183a7203289d432ed540f2cfa6227f153f19d4b)) * **deps:** bump @angular/cli from 11.1.4 to 11.2.0 in /frontend ([5efdefd](https://github.com/softrams/bulwark/commit/5efdefdd07dc0319054bf78784c0c9e3edb9ee7c)) * **deps:** bump @angular/cli from 11.2.0 to 11.2.1 in /frontend ([4f33044](https://github.com/softrams/bulwark/commit/4f33044754b8591ba79652418300ea1f984449bd)) * **deps:** bump @ng-select/ng-select from 6.0.0 to 6.1.0 in /frontend ([2ed6465](https://github.com/softrams/bulwark/commit/2ed64654f6a235c50da8901bd74f7e32f21d4622)) * **deps:** bump concurrently from 5.3.0 to 6.0.0 ([710b652](https://github.com/softrams/bulwark/commit/710b652ddcc0651f8943f25e6fca6461f09e3952)) * **deps:** bump core-js from 3.8.3 to 3.9.0 in /frontend ([58d28ef](https://github.com/softrams/bulwark/commit/58d28ef00548c2919c8119a3850b3f85e2fa63ff)) * **deps:** bump mime-types from 2.1.28 to 2.1.29 ([a1b5ad9](https://github.com/softrams/bulwark/commit/a1b5ad9e8d60d95a06e55e400e4765f179a2b694)) * **deps:** bump ngx-markdown from 11.0.1 to 11.1.0 in /frontend ([589fe38](https://github.com/softrams/bulwark/commit/589fe38acb5facc458c579f75fdada71ca62c0b0)) * **deps:** bump nodemailer from 6.4.17 to 6.4.18 ([81f845c](https://github.com/softrams/bulwark/commit/81f845ce080047191a3e61c55a6b6b9311ef1f55)) * **deps:** bump primeng from 11.2.0 to 11.2.1 in /frontend ([ae7815f](https://github.com/softrams/bulwark/commit/ae7815fa8ffe9ac76e581c0e1a696a537a3d07dc)) * **deps:** bump primeng from 11.2.1 to 11.2.2 in /frontend ([22bc78e](https://github.com/softrams/bulwark/commit/22bc78e64ec67d731c3daccb1509ca561b5a8ce7)) * **deps:** bump primeng from 11.2.2 to 11.2.3 in /frontend ([0cef0b7](https://github.com/softrams/bulwark/commit/0cef0b7cdda0d0986319823a0c8dfa6069cece78)) * **deps:** bump typeorm from 0.2.30 to 0.2.31 ([07baf54](https://github.com/softrams/bulwark/commit/07baf546f05187b28d5e03142487ca17ab9a7f88)) * **deps:** bump typescript from 4.0.5 to 4.0.7 in /frontend ([3d72b16](https://github.com/softrams/bulwark/commit/3d72b16035e92adbe219b0e48a7694c26a8d2ddf)) * **deps-dev:** bump @angular/language-service in /frontend ([b7c03b0](https://github.com/softrams/bulwark/commit/b7c03b03ec2b769afe5fbc68d5552f7b475ca6f0)) * **deps-dev:** bump @angular/language-service in /frontend ([b914a9d](https://github.com/softrams/bulwark/commit/b914a9d1f4178f1e37a10a66efe08829e3c8c4e4)) * **deps-dev:** bump @babel/core from 7.12.13 to 7.12.16 ([0c682df](https://github.com/softrams/bulwark/commit/0c682df0f1ae8b73645f41b18ad5b8dce29014e3)) * **deps-dev:** bump @babel/core from 7.12.16 to 7.12.17 ([49dd4d4](https://github.com/softrams/bulwark/commit/49dd4d4d41a6b869fda026b8422a8febafaf7f1b)) * **deps-dev:** bump @babel/preset-env from 7.12.13 to 7.12.16 ([eb6f370](https://github.com/softrams/bulwark/commit/eb6f37013bf27789dd989ae25899345076515c1e)) * **deps-dev:** bump @babel/preset-env from 7.12.16 to 7.12.17 ([a1ddf66](https://github.com/softrams/bulwark/commit/a1ddf66ea5c72b8db3df7385ca35f4026d88829e)) * **deps-dev:** bump @babel/preset-typescript from 7.12.13 to 7.12.16 ([0875b9c](https://github.com/softrams/bulwark/commit/0875b9cc5f293bf13542195043d1a826239d6a1a)) * **deps-dev:** bump @babel/preset-typescript from 7.12.16 to 7.12.17 ([5096b4f](https://github.com/softrams/bulwark/commit/5096b4f2314a3382d0afeb0bcbfd4453bda6e65a)) * **deps-dev:** bump @types/jasmine from 3.6.3 to 3.6.4 in /frontend ([7b4fb70](https://github.com/softrams/bulwark/commit/7b4fb70051bd7af9cfb91bf58bfe422819bd48d3)) * **deps-dev:** bump @types/node from 14.14.25 to 14.14.28 in /frontend ([5219d46](https://github.com/softrams/bulwark/commit/5219d46e6f9f0777c9695d968bac298539ee8b8b)) * **deps-dev:** bump @types/node from 14.14.28 to 14.14.31 in /frontend ([e1ffb08](https://github.com/softrams/bulwark/commit/e1ffb08ac0887bb3c947f71edcbc815bda3a7fb3)) * **deps-dev:** bump husky from 4.3.8 to 5.0.9 ([760fc1f](https://github.com/softrams/bulwark/commit/760fc1f6e5e8dc05392c54715c7d1e99e51a5a72)) * **deps-dev:** bump husky from 5.0.9 to 5.1.0 ([b0f7733](https://github.com/softrams/bulwark/commit/b0f7733bfecf5056290129d413d206e06066bd09)) * **deps-dev:** bump karma from 6.1.0 to 6.1.1 in /frontend ([dab7567](https://github.com/softrams/bulwark/commit/dab75670900e43c74286b084189c942b7bcdbc50)) * **deps-dev:** bump sqlite3 from 5.0.1 to 5.0.2 ([269859e](https://github.com/softrams/bulwark/commit/269859edef5dca35beb9c9b5c07b6b300cb73ecb)) * **deps-dev:** bump standard-version from 9.1.0 to 9.1.1 ([4a546fa](https://github.com/softrams/bulwark/commit/4a546fad84f7eabecee3fcb070f95211a4acb25d)) * **deps-dev:** bump ts-jest from 26.5.0 to 26.5.1 ([f7fc6d3](https://github.com/softrams/bulwark/commit/f7fc6d30c1bd87595b8b1119df83e8b9b3e20118)) * **deps-dev:** bump typescript from 4.1.3 to 4.1.5 ([979eddb](https://github.com/softrams/bulwark/commit/979eddba0e897528ba7b9ebeeb9ea7fbe35d3253)) * **package.json:** update master with main ([29bc8b8](https://github.com/softrams/bulwark/commit/29bc8b8a75d9889ace85566dcd8b68070369f5c4)) ### Build System * **dependabot.yml:** removing Bill from dependabot config ([c8b2f9d](https://github.com/softrams/bulwark/commit/c8b2f9de379ae554c9ce21366f118e8ef3b8a3b6)) ### Docs * **readme.md:** update README.md ([3b778d2](https://github.com/softrams/bulwark/commit/3b778d29424bcbe6f40f8d4d37d3e9a9ff137821)) ### [7.0.4](https://github.com/softrams/bulwark/compare/v7.0.3...v7.0.4) (2021-02-10) ### Bug Fixes * **assessment.controller.ts:** fixed authz issue ([7436e41](https://github.com/softrams/bulwark/commit/7436e41cc38fce6e34031aa43d7de81eb82cd1e2)) ### [7.0.3](https://github.com/softrams/bulwark/compare/v7.0.2...v7.0.3) (2021-02-10) ### Bug Fixes * **assessment/vulnerability.controller.ts:** fixed authz tester roles issue with vulnerabilities ([a2febf7](https://github.com/softrams/bulwark/commit/a2febf7314b7bc349cc97ba696d106472e5f25da)) ### [7.0.2](https://github.com/softrams/bulwark/compare/v7.0.1...v7.0.2) (2021-02-10) ### Bug Fixes * **vulnerability.controller.ts:** fixed tester access error ([ddc7c02](https://github.com/softrams/bulwark/commit/ddc7c02c6419cc63c8182b486bee0152c6b786eb)) ### [7.0.1](https://github.com/softrams/bulwark/compare/v7.0.0...v7.0.1) (2021-02-10) ### Bug Fixes * **src:** fixed tester authz. Fixed no organization in system with admin. fixed the config script ([ff4696e](https://github.com/softrams/bulwark/commit/ff4696e88fcb7dcf892f9639dfc7d63d96500595)) ## [7.0.0](https://github.com/softrams/bulwark/compare/v6.4.8...v7.0.0) (2021-02-10) ### ⚠ BREAKING CHANGES * **api-key.controller:** New `API_KEY` column for secret key * **api-key.controller:** New API_KEY table with many-to-one relation with the User table * **package.json:** Added additional process env variables. Puppeteer functionality has been updated to use these new process env variables. * **authentication/authorization for admins:** Updated many-to-many relationship between Team and User entities * **team.ts, roles-enum.ts, config.controller:** Added Team entity to database * **reportaudit.ts:** Added new database table ### Features * **api-key.controller:** added `SecretKey` column to API_KEY table ([6bdfaca](https://github.com/softrams/bulwark/commit/6bdfaca609d71ea6c64bb844411747e1160d8092)), closes [#483](https://github.com/softrams/bulwark/issues/483) * **api-key.controller:** implement API key integration ([bb34a64](https://github.com/softrams/bulwark/commit/bb34a6463b11d6122fefcf19d71b3186e7c3c8e1)), closes [#483](https://github.com/softrams/bulwark/issues/483) * **assessment.controller:** wire roles into assessment controller and component ([0660050](https://github.com/softrams/bulwark/commit/06600501d7e9a93acd6d1817ec2d820bd0f657e6)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **asset team entities:** add many-to-many relationship between Team and Asset ([5d27d5c](https://github.com/softrams/bulwark/commit/5d27d5cc972380a4d1825f4341df67e3d4850593)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **authentication/authorization for admins:** update many-to-many relationship user and teams ([f22e7de](https://github.com/softrams/bulwark/commit/f22e7dee01f667345e558ea96c84ad917b2fb63a)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **dashboard.component org-form:** add role checks to org controller api's and angular components ([64de7a0](https://github.com/softrams/bulwark/commit/64de7a0dffd8136af05c254ed7d2941c47851994)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **jwt.middleware.ts:** integrate roles into jwt middleware ([54e4647](https://github.com/softrams/bulwark/commit/54e46476441ce6c0cde3e89b80606026694f484a)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **organization team entity:** add many-to-one relation ship between Team and Organization Entities ([1dfc435](https://github.com/softrams/bulwark/commit/1dfc43514c37e284b182714b98365325bfc1300a)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **report-audit.controller:** implement report auditing function ([7a86b96](https://github.com/softrams/bulwark/commit/7a86b96ca6f68502a795e7623628d0735d2206b3)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **reportaudit.ts:** create ReportAudit table. Implement insert function ([a0e766d](https://github.com/softrams/bulwark/commit/a0e766d1e26b56647ed6b01d3f52ba01cb8a1fe6)), closes [#497](https://github.com/softrams/bulwark/issues/497) * **role.utility.ts:** implement role checking for organization ([ff8b5b8](https://github.com/softrams/bulwark/commit/ff8b5b845bfb3f720c5b5b0aa89e7067bccc3d86)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **tea-form.component:** initial commit for team-form component ([67a4e6f](https://github.com/softrams/bulwark/commit/67a4e6fd623baa610e8f56512f24a116900c5f68)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.component:** implement team component ([ad4d045](https://github.com/softrams/bulwark/commit/ad4d045eb7d5d4c301447de06f1a156a48230891)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.component:** wire team form to api ([5a2fc4e](https://github.com/softrams/bulwark/commit/5a2fc4eb61c2772bb17e0301a42025935f741341)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller:** implement additional Team APIs ([71292f1](https://github.com/softrams/bulwark/commit/71292f132e432c984a5750a21651c36d1d5e1117)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller:** implement API routes for getMyTeams and deleteTeam ([842ca1e](https://github.com/softrams/bulwark/commit/842ca1efa23980ef27d997a36f67960e85aa3db2)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller:** implement route functions for Add/Remove team assets ([6e68127](https://github.com/softrams/bulwark/commit/6e68127aee505d45a523f3ef585d6b31ced75956)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller:** implment updateTeamInfo function ([c7761ea](https://github.com/softrams/bulwark/commit/c7761ea57d83a9e98cbdf9fc479371f729a697a5)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller.ts:** added helper functions to team controller ([13a2cca](https://github.com/softrams/bulwark/commit/13a2ccaa9153e0e45c505efa4cddf7bb919397e1)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.ts:** remove single asset column ([89e1e84](https://github.com/softrams/bulwark/commit/89e1e840461921486a3984412260d322c9a8a89d)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.ts, roles-enum.ts, config.controller:** add Team entity to database ([71469cc](https://github.com/softrams/bulwark/commit/71469ccaae2620e0f7b686828579a813d26431f2)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **user-management:** update user management component. Update team component ([4fa4425](https://github.com/softrams/bulwark/commit/4fa4425444f4ca423f7e5abe3867513a2046ffd5)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **user-management.component:** initial commit for user-management.component ([7ca32e2](https://github.com/softrams/bulwark/commit/7ca32e2d68a7e4ba5899dde974e5e5b7f26446f5)), closes [#553](https://github.com/softrams/bulwark/issues/553) * **user-management.component:** initial commit of user-management template ([b373629](https://github.com/softrams/bulwark/commit/b37362934ddbf9a6a5015fc263715e4da768cac1)), closes [#554](https://github.com/softrams/bulwark/issues/554) * **user-management.component:** updated user-management.component template and component ([44bd8ff](https://github.com/softrams/bulwark/commit/44bd8ffb8d41645f5014e7bf86b573f8656625b5)), closes [#553](https://github.com/softrams/bulwark/issues/553) * **user-profile.component:** add teams to user profile ([aba66fc](https://github.com/softrams/bulwark/commit/aba66fc231ea1e3f0cbade7a4228bce3e1159294)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **user.controller.ts:** implement API to create a user ([37aea71](https://github.com/softrams/bulwark/commit/37aea71654a8245f1e82aa78f8bb601e2de71a6d)), closes [#553](https://github.com/softrams/bulwark/issues/553) * **vuln.controller:** wire roles into vuln controller and component ([d4db2fc](https://github.com/softrams/bulwark/commit/d4db2fccea945b9269bb4d3147dd0cf0125f248a)), closes [#484](https://github.com/softrams/bulwark/issues/484) ### Bug Fixes * no Vulns Blank page ([6a4247a](https://github.com/softrams/bulwark/commit/6a4247a7eef8fe7cb5e667720bf6b25da6173067)) * **app.ts:** cSP error fix ([b6a02f8](https://github.com/softrams/bulwark/commit/b6a02f8c5e6943cbae4735c43d0f6fb3fd96ff13)), closes [#566](https://github.com/softrams/bulwark/issues/566) * **app.ts:** fix CSP errors ([594628c](https://github.com/softrams/bulwark/commit/594628c0672cfbe647be21a2fbefe7b7120ef0ff)), closes [#566](https://github.com/softrams/bulwark/issues/566) * **docker-compose.yml:** fix docker-compose bug ([58a549f](https://github.com/softrams/bulwark/commit/58a549f397d54fab63352b20b426fdbb4439c090)) * **fix csp errors:** cSP errors were generated in console ([801c9a3](https://github.com/softrams/bulwark/commit/801c9a3742ffb515a90880147d2d1ab97c67774f)) * **frontend/package.json:** update script to use different angular envv ([8c2481e](https://github.com/softrams/bulwark/commit/8c2481ec7297033ea27a85690c80e89bed89ebfe)), closes [#567](https://github.com/softrams/bulwark/issues/567) * **package.json:** updated package.json script ([3e201eb](https://github.com/softrams/bulwark/commit/3e201eb9b14facd7808874d69604c4beee9c909e)), closes [#567](https://github.com/softrams/bulwark/issues/567) * **role.utility.ts:** fixed role bug ([24db31a](https://github.com/softrams/bulwark/commit/24db31af83bf687c74ba10ebef0c7dcbad6f6cc5)) * **setenv.ts:** implement dynamic port and IP address based on env var ([f52e696](https://github.com/softrams/bulwark/commit/f52e69673cdbf0df0a533437d62f198d8afb69b9)), closes [#567](https://github.com/softrams/bulwark/issues/567) * **team-form.component:** fix undefined bug if team does not have org ([64e15f1](https://github.com/softrams/bulwark/commit/64e15f1e168327c900f70e24d7a08ddebc2559be)) ### Tests * **api-key.controller.spec.ts:** implement unit tests for API key controller ([48df0ef](https://github.com/softrams/bulwark/commit/48df0efa8561be549e88e4306692f58f5c44a0da)), closes [#483](https://github.com/softrams/bulwark/issues/483) * **asset.controller:** implemented roles into asset controller ([a63b8dd](https://github.com/softrams/bulwark/commit/a63b8dd53c4d36483ceed188c717af6d06561c5e)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **config.controller.spec.ts:** implement unit tests for Teams ([b95e398](https://github.com/softrams/bulwark/commit/b95e39804f7be517262e4fc6a7961ce1d54b184f)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **team.controller:** fix broken unit tests ([ad9c3c9](https://github.com/softrams/bulwark/commit/ad9c3c911456b36b65ed486bd044df7465230d99)), closes [#484](https://github.com/softrams/bulwark/issues/484) ### Others * **.prettierignore:** fix merge conflict ([953def8](https://github.com/softrams/bulwark/commit/953def8b4eb56227d4e486e3357c7193256249b9)) * **.prettierignore:** fix merge conflicts. fix prettier config error ([baf5767](https://github.com/softrams/bulwark/commit/baf57670053079ddfd52435a2a5e19297ab9efde)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **deps:** bump @angular-devkit/build-angular in /frontend ([c2c2184](https://github.com/softrams/bulwark/commit/c2c2184b007eb7b85b33af24db0aba2c938a1be0)) * **deps:** bump @angular-devkit/build-angular in /frontend ([7ec05d3](https://github.com/softrams/bulwark/commit/7ec05d34b112779549bd41506eb6de17dc8510f4)) * **deps:** bump @angular-devkit/build-angular in /frontend ([c100c14](https://github.com/softrams/bulwark/commit/c100c147ca781676382a82e86eb3e70112f350f2)) * **deps:** bump @angular/cdk from 11.0.3 to 11.0.4 in /frontend ([4e2db70](https://github.com/softrams/bulwark/commit/4e2db700078e161eb0a80189801b49bfc765264d)) * **deps:** bump @angular/cdk from 11.0.4 to 11.1.0 in /frontend ([d4829cc](https://github.com/softrams/bulwark/commit/d4829ccf8c5b4303427696fa4b369793326e7b6b)) * **deps:** bump @angular/cdk from 11.1.0 to 11.1.2 in /frontend ([63ee378](https://github.com/softrams/bulwark/commit/63ee378b8c750210053cde47a4ac765156a14a05)) * **deps:** bump @angular/cli from 11.0.6 to 11.0.7 in /frontend ([22fd855](https://github.com/softrams/bulwark/commit/22fd85561ef8d1147f977e63fdd44f85f838861e)) * **deps:** bump @angular/cli from 11.0.7 to 11.1.1 in /frontend ([df3c6c5](https://github.com/softrams/bulwark/commit/df3c6c561e57fd5eceee1d455e82b44619dd17ab)) * **deps:** bump @angular/cli from 11.1.1 to 11.1.4 in /frontend ([a3ecf96](https://github.com/softrams/bulwark/commit/a3ecf9652a037a8ed7bb30c5a9b6b6c9c6f258f9)) * **deps:** bump @ng-select/ng-select from 5.0.15 to 5.1.0 in /frontend ([0d1c7a7](https://github.com/softrams/bulwark/commit/0d1c7a79e2e504e0aa77cc88741f2b31cc29ab3b)) * **deps:** bump @ng-select/ng-select from 5.1.0 to 6.0.0 in /frontend ([d4fa635](https://github.com/softrams/bulwark/commit/d4fa635cf360959565a62b45d676d3b5c3875cce)) * **deps:** bump @types/express from 4.17.9 to 4.17.11 ([fcfd3ee](https://github.com/softrams/bulwark/commit/fcfd3eef5ce03f04bc1ca4495308e867f476275a)) * **deps:** bump class-validator from 0.12.2 to 0.13.1 ([404493a](https://github.com/softrams/bulwark/commit/404493abb16837298ac8f5863d53e22a68e808d9)) * **deps:** bump core-js from 3.8.2 to 3.8.3 in /frontend ([1aff860](https://github.com/softrams/bulwark/commit/1aff860c3dacbe905bea8bc44e6aede86af343d9)) * **deps:** bump helmet from 4.3.1 to 4.4.0 ([349d1b6](https://github.com/softrams/bulwark/commit/349d1b68976d36232f3f0be86474dc818b78df5e)) * **deps:** bump helmet from 4.4.0 to 4.4.1 ([92b5ae9](https://github.com/softrams/bulwark/commit/92b5ae98ba0bdee4a218805a8042a61c2d14538e)) * **deps:** bump jira2md from 2.0.4 to 2.0.5 ([2e0fd53](https://github.com/softrams/bulwark/commit/2e0fd532ad67c33f545bc225ac5103ecfe0fa3e9)) * **deps:** bump primeng from 11.0.0 to 11.1.0 in /frontend ([474f4aa](https://github.com/softrams/bulwark/commit/474f4aafec9e3b4c75a5e8621fa8d9fc047bf622)) * **deps:** bump primeng from 11.1.0 to 11.2.0 in /frontend ([697aa33](https://github.com/softrams/bulwark/commit/697aa33ad9f8eed67f1a2bc09d3e9ba562a131ae)) * **deps:** bump typeorm from 0.2.29 to 0.2.30 ([a3c091c](https://github.com/softrams/bulwark/commit/a3c091c54ad044980eed8c1e8d7434960a77618e)) * **deps-dev:** bump @angular/language-service in /frontend ([b104657](https://github.com/softrams/bulwark/commit/b104657d512caaaa0ed3c77f09d78774e534e0b7)) * **deps-dev:** bump @angular/language-service in /frontend ([5dc85dd](https://github.com/softrams/bulwark/commit/5dc85dd16c9ac7765960e21a8b1b738367be7fd7)) * **deps-dev:** bump @angular/language-service in /frontend ([f418dd1](https://github.com/softrams/bulwark/commit/f418dd1e0c4d79a2d8651832b099b2b5d18990cf)) * **deps-dev:** bump @babel/core from 7.12.10 to 7.12.13 ([6d7cf75](https://github.com/softrams/bulwark/commit/6d7cf7516b591881a5b9f3e65e776bc8c5acfd22)) * **deps-dev:** bump @babel/preset-env from 7.12.11 to 7.12.13 ([042cbb2](https://github.com/softrams/bulwark/commit/042cbb25c6f9b976ec7caff7a3dc52c6c359c39d)) * **deps-dev:** bump @babel/preset-typescript from 7.12.7 to 7.12.13 ([acaa0e7](https://github.com/softrams/bulwark/commit/acaa0e73b1c28ae2ad30b34d7c14e0ad42d59137)) * **deps-dev:** bump @types/jasmine from 3.6.2 to 3.6.3 in /frontend ([3eb9afa](https://github.com/softrams/bulwark/commit/3eb9afa34b92a973d875f6d4638f327accf4c5b2)) * **deps-dev:** bump @types/node from 14.14.20 to 14.14.21 in /frontend ([6d5d746](https://github.com/softrams/bulwark/commit/6d5d7462e132f847a7b13515d263bde71085a061)) * **deps-dev:** bump @types/node from 14.14.21 to 14.14.22 in /frontend ([8810309](https://github.com/softrams/bulwark/commit/8810309ee41068fcdb5ffbf0346ab72d6204b290)) * **deps-dev:** bump @types/node from 14.14.22 to 14.14.25 in /frontend ([90f8f93](https://github.com/softrams/bulwark/commit/90f8f937247ea4490909bc444f6fa18a23cf98ad)) * **deps-dev:** bump highlight.js from 10.5.0 to 10.6.0 ([a0c45be](https://github.com/softrams/bulwark/commit/a0c45be652e787029694d4055f63ce7019d2608d)) * **deps-dev:** bump husky from 4.3.7 to 4.3.8 ([c2ea443](https://github.com/softrams/bulwark/commit/c2ea443fbbdcea71836db4a32e97add1e4f4f160)) * **deps-dev:** bump karma from 5.2.3 to 6.0.0 in /frontend ([4e6b2b5](https://github.com/softrams/bulwark/commit/4e6b2b5524a3637798344ce2ddda2caa64d5cc75)) * **deps-dev:** bump karma from 6.0.0 to 6.0.1 in /frontend ([6c5f2ce](https://github.com/softrams/bulwark/commit/6c5f2ce9aea265d7ddddbe7402eb2dfbcf9444d3)) * **deps-dev:** bump karma from 6.0.1 to 6.1.0 in /frontend ([48c7b74](https://github.com/softrams/bulwark/commit/48c7b74f473478995251a6854f28b4ee482ffe53)) * **deps-dev:** bump lint-staged from 10.5.3 to 10.5.4 ([2e8aa42](https://github.com/softrams/bulwark/commit/2e8aa4249744a846e79c286b887ef6651181b87b)) * **deps-dev:** bump ts-jest from 26.4.4 to 26.5.0 ([9232d26](https://github.com/softrams/bulwark/commit/9232d2657c228f71839baed49915994344d4d910)) * **fixing merge conflicts:** fix merge conflicts ([a2c2148](https://github.com/softrams/bulwark/commit/a2c21483fd61227a8afa8dac10f9d197d2cfbd97)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **global:** merge [#553](https://github.com/softrams/bulwark/issues/553) branch into roles ([28414e1](https://github.com/softrams/bulwark/commit/28414e11348056d7c46ad35ae6c899e95b27b29e)), closes [#484](https://github.com/softrams/bulwark/issues/484) * **merge conflicts:** fix merge conflicts ([a431af0](https://github.com/softrams/bulwark/commit/a431af0e9efc8a1d0425c690c615ab4d0c1ab30e)), closes [#567](https://github.com/softrams/bulwark/issues/567) * **setenv.ts:** update console.info and documentation ([3aa0e94](https://github.com/softrams/bulwark/commit/3aa0e94b526932fdf70fde6901e5a2ffa33a4a55)), closes [#567](https://github.com/softrams/bulwark/issues/567) ### Docs * **readme.md:** update README.md ([ba3a77d](https://github.com/softrams/bulwark/commit/ba3a77d138bc43e81b119ef0c781c121b3d580fb)), closes [#484](https://github.com/softrams/bulwark/issues/484) ### Code Refactoring * **config.controller:** remove global teams ([ab8bd9b](https://github.com/softrams/bulwark/commit/ab8bd9b8be68aa951c628eda651a5ca446a5edfe)), closes [#484](https://github.com/softrams/bulwark/issues/484) ### Build System * **.gitignore:** delete generated angular env files ([3be81a2](https://github.com/softrams/bulwark/commit/3be81a2395c933efd71e1b6b8453b84ad34fc050)), closes [#567](https://github.com/softrams/bulwark/issues/567) ### [6.4.8](https://github.com/softrams/bulwark/compare/v6.4.7...v6.4.8) (2021-01-14) ### Build System * **dockerfile:** remove unneeded packages ([b6c9de1](https://github.com/softrams/bulwark/commit/b6c9de185359cf3c7028492d780f670484a83f53)) ### [6.4.7](https://github.com/softrams/bulwark/compare/v6.4.6...v6.4.7) (2021-01-14) ### Build System * **dockerfile:** add python to container ([6224469](https://github.com/softrams/bulwark/commit/62244692a50beb155073192656f2a03456dc61ff)) ### [6.4.6](https://github.com/softrams/bulwark/compare/v6.4.5...v6.4.6) (2021-01-14) ### Build System * **dockerfile:** add rimraf library ([70e63ba](https://github.com/softrams/bulwark/commit/70e63bab5787d5d40f81c402ea2a6653311e2e52)) ### [6.4.5](https://github.com/softrams/bulwark/compare/v6.4.4...v6.4.5) (2021-01-14) ### Build System * **dockerfile:** install typescript globally ([eac63c8](https://github.com/softrams/bulwark/commit/eac63c8d4a19668f2c3f6e70445a2f43360bd51f)) ### [6.4.4](https://github.com/softrams/bulwark/compare/v6.4.3...v6.4.4) (2021-01-14) ### Build System * **dockerfile:** root !== ROOT ([2f22d20](https://github.com/softrams/bulwark/commit/2f22d20fbc027e01427339fa068c3ab18ffe7f7b)) ### [6.4.3](https://github.com/softrams/bulwark/compare/v6.4.2...v6.4.3) (2021-01-14) ### Build System - **dockerfile:** add USER ROOT ([3bf6e0e](https://github.com/softrams/bulwark/commit/3bf6e0e240db62090fb1adf4b8aa2f4f3fb78284)) - **dockerfile:** update npm install ([ca447d3](https://github.com/softrams/bulwark/commit/ca447d35d1fc9cae2efe6b8876faa34df9ed1540)) ### [6.4.2](https://github.com/softrams/bulwark/compare/v6.4.1...v6.4.2) (2021-01-12) ### Bug Fixes - **vulnerability.component.html:** fixed Risk and Status undefined error ([bbc407f](https://github.com/softrams/bulwark/commit/bbc407fa03d3294744595dc26af0421f168c5589)), closes [#539](https://github.com/softrams/bulwark/issues/539) ### Others - **package-lock.json:** commit package-lock.json ([22caf95](https://github.com/softrams/bulwark/commit/22caf95efd36ee10a477b5f5cf756f4697c776fc)) ### [6.4.1](https://github.com/softrams/bulwark/compare/v6.4.0...v6.4.1) (2021-01-11) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([b388648](https://github.com/softrams/bulwark/commit/b388648a4314e9efba338c39ccbe0516319f5675)) - **deps:** bump @angular/cli from 11.0.5 to 11.0.6 in /frontend ([ca9ef69](https://github.com/softrams/bulwark/commit/ca9ef69a6e84607e8c8a47a6ff9ac1ca63e6d803)) - **deps:** bump @ng-select/ng-select in /frontend ([ade5b50](https://github.com/softrams/bulwark/commit/ade5b508303e8b13b174c918208125cb6c3bcd40)) - **deps:** bump tslib from 2.0.3 to 2.1.0 in /frontend ([45b481e](https://github.com/softrams/bulwark/commit/45b481eef0f7012f71c2f20da045251201ce75b8)) - **deps-dev:** bump @angular/language-service in /frontend ([0597a7f](https://github.com/softrams/bulwark/commit/0597a7fea71bdd31c0873630fa52a6f3959601cc)) - **deps-dev:** bump @types/jest from 26.0.19 to 26.0.20 ([eab0403](https://github.com/softrams/bulwark/commit/eab040333f752b96b970bf7fbb67781f3061dbb0)) - **deps-dev:** bump @types/node from 14.14.19 to 14.14.20 in /frontend ([6c95f9c](https://github.com/softrams/bulwark/commit/6c95f9c8994fb5d04881846f8465d7b4dcb45383)) - **deps-dev:** bump husky from 4.3.6 to 4.3.7 ([db63af4](https://github.com/softrams/bulwark/commit/db63af4c13f76386a6a7ad331aa62045ba4edfad)) - **deps-dev:** bump sqlite3 from 5.0.0 to 5.0.1 ([45e0a8c](https://github.com/softrams/bulwark/commit/45e0a8c4452e994728295e8f0ee4479c1d077b23)) ## [6.4.0](https://github.com/softrams/bulwark/compare/v6.3.1...v6.4.0) (2021-01-04) ### Features - **adminsitration area:** adding Administration Area ([e5d91b2](https://github.com/softrams/bulwark/commit/e5d91b21459a5460ea848d3530085596a15b7cbf)), closes [#491](https://github.com/softrams/bulwark/issues/491) - **invite-user.component:** update look and feel of invite-user and settings component ([cdcf572](https://github.com/softrams/bulwark/commit/cdcf572ed6022ba5acd0e477f0580f8013e46349)) - **package.json:** add prettier pre-commit hook ([7602dfb](https://github.com/softrams/bulwark/commit/7602dfb39f95d0e4349ee8dde8da885508c17b7e)), closes [#509](https://github.com/softrams/bulwark/issues/509) - **prettier:** adding prettier ([eb44eb1](https://github.com/softrams/bulwark/commit/eb44eb111f7d028a9282071ac67fe9b2a67b0b5d)), closes [#508](https://github.com/softrams/bulwark/issues/508) - **prettier:** update prettier config ([ba2c5f1](https://github.com/softrams/bulwark/commit/ba2c5f188f0248d05231c4b5abe8c68ccf154219)), closes [#509](https://github.com/softrams/bulwark/issues/509) - **report component:** color changes to pie chart ([7269d62](https://github.com/softrams/bulwark/commit/7269d6282c46a4284ad58dd008ca73f982c3deae)), closes [#487](https://github.com/softrams/bulwark/issues/487) - **report component:** color changes to the pie chart ([707271e](https://github.com/softrams/bulwark/commit/707271e409b3be5a28731fedd6b88fac1a7f860d)), closes [#487](https://github.com/softrams/bulwark/issues/487) - **report.component:** implement overall risk sort ([e31c76c](https://github.com/softrams/bulwark/commit/e31c76ce26f8ce22b3af616650414555cf1a1b1b)), closes [#482](https://github.com/softrams/bulwark/issues/482) ### Bug Fixes - **assessments.component.ts:** fixed build break due to deprecated FilterUtils ([d7c2ae3](https://github.com/softrams/bulwark/commit/d7c2ae32cf274c27e1c55284e4765543d5449dbf)) ### Others - **assessments.component.ts:** remove library and console.log ([0cab4ea](https://github.com/softrams/bulwark/commit/0cab4ea553e6241a89df73d0c04e6c5154b45815)) - **deps:** bump @angular-devkit/build-angular in /frontend ([b33ce85](https://github.com/softrams/bulwark/commit/b33ce85f8b2e9199c3a12a3c7a8de5cd6f7d0d15)) - **deps:** bump @angular/cdk from 11.0.1 to 11.0.2 in /frontend ([49da433](https://github.com/softrams/bulwark/commit/49da433b38adb03d86aa0680e85019f01bfae331)) - **deps:** bump @angular/cdk from 11.0.2 to 11.0.3 in /frontend ([786c8f0](https://github.com/softrams/bulwark/commit/786c8f09d801cd8cd0a0a3758d65a23b121b334b)) - **deps:** bump @angular/cli from 11.0.3 to 11.0.5 in /frontend ([d9033e4](https://github.com/softrams/bulwark/commit/d9033e4306de09eff073cec9355c097b779c00e5)) - **deps:** bump @ng-select/ng-select from 5.0.9 to 5.0.11 in /frontend ([b1428ab](https://github.com/softrams/bulwark/commit/b1428ab991b37c18a3805caa237354be2239bf86)) - **deps:** bump @ng-select/ng-select in /frontend ([1cdc8ab](https://github.com/softrams/bulwark/commit/1cdc8ab676bbc2320eafe04d746d3bb2bce4382f)) - **deps:** bump core-js from 3.8.1 to 3.8.2 in /frontend ([6c05e06](https://github.com/softrams/bulwark/commit/6c05e06b90ef53ad168bbb7d638d23e62b38515b)) - **deps:** bump helmet from 4.2.0 to 4.3.1 ([2f0e7fb](https://github.com/softrams/bulwark/commit/2f0e7fbf405dcf4bf758f248fbc6e313a0279d72)) - **deps:** bump mime-types from 2.1.27 to 2.1.28 ([0c85c5f](https://github.com/softrams/bulwark/commit/0c85c5f9904219fb4dbf89eb935fe4a2208e05e3)) - **deps:** bump nodemailer from 6.4.16 to 6.4.17 ([3c7456d](https://github.com/softrams/bulwark/commit/3c7456df86e7dcc5c29ab8b6da3034ddaf04f07c)) - **deps:** bump primeng from 10.0.3 to 11.0.0 in /frontend ([992dc1a](https://github.com/softrams/bulwark/commit/992dc1a6c6d5d12c406f1cfd1fa1ba6eba65a6a4)) - **deps:** bump ts-node from 9.1.0 to 9.1.1 ([24f3afa](https://github.com/softrams/bulwark/commit/24f3afaec8a4e010e4382ce48243b37a7c3c3ad7)) - **deps:** bump uuid from 8.3.1 to 8.3.2 ([a9d5a1b](https://github.com/softrams/bulwark/commit/a9d5a1b3854043b2e4c2852a9056103de2f692a0)) - **deps-dev:** bump @angular/language-service in /frontend ([6b9bb89](https://github.com/softrams/bulwark/commit/6b9bb89cc1ece2f94bd3b1b186ea09159d524314)) - **deps-dev:** bump @angular/language-service in /frontend ([65f5818](https://github.com/softrams/bulwark/commit/65f5818de0ebe716348a99877e68b7fc8a676387)) - **deps-dev:** bump @babel/core from 7.12.9 to 7.12.10 ([ba5a326](https://github.com/softrams/bulwark/commit/ba5a32667b6b8c6a7b8a7c54bf2e8cedc3045b91)) - **deps-dev:** bump @babel/preset-env from 7.12.7 to 7.12.11 ([dde25dd](https://github.com/softrams/bulwark/commit/dde25dde0fd497552ca931c6dbd43c53eab95790)) - **deps-dev:** bump @types/jest from 26.0.16 to 26.0.19 ([463675c](https://github.com/softrams/bulwark/commit/463675cf2f763b534d16a070a780155c34742373)) - **deps-dev:** bump @types/node from 14.14.10 to 14.14.13 in /frontend ([424df1c](https://github.com/softrams/bulwark/commit/424df1c5566ac300c8237e90f56794ef06aad476)) - **deps-dev:** bump @types/node from 14.14.13 to 14.14.14 in /frontend ([5b61ed8](https://github.com/softrams/bulwark/commit/5b61ed8774a1130d954513d3fa788ca4d5ed588e)) - **deps-dev:** bump @types/node from 14.14.14 to 14.14.16 in /frontend ([d322e2d](https://github.com/softrams/bulwark/commit/d322e2d6316ec92dcdfcef1cf988468f618256f1)) - **deps-dev:** bump @types/node from 14.14.16 to 14.14.19 in /frontend ([9b41c20](https://github.com/softrams/bulwark/commit/9b41c20204699310a1b65a09a31b6052d5a68765)) - **deps-dev:** bump highlight.js from 10.4.1 to 10.5.0 ([2fbf9c6](https://github.com/softrams/bulwark/commit/2fbf9c69aace53f3abb55af007c70a6aacf71a46)) - **deps-dev:** bump husky from 4.3.0 to 4.3.6 ([7266035](https://github.com/softrams/bulwark/commit/7266035a5ecf43389c3d9f2f8b79c39866695cf3)) - **deps-dev:** bump karma from 5.1.1 to 5.2.3 in /frontend ([a58dbf9](https://github.com/softrams/bulwark/commit/a58dbf9af33270f5d8d9d153ce73cc9e1c74c9dd)) - **deps-dev:** bump mock-express-response from 0.2.2 to 0.3.0 ([e9588b8](https://github.com/softrams/bulwark/commit/e9588b884b6656aa59058c5de58bd539da61c6f0)) - **deps-dev:** bump standard-version from 9.0.0 to 9.1.0 ([a497d0e](https://github.com/softrams/bulwark/commit/a497d0e14d1fb8a9adebde6d2da836efde70e48f)) - **deps-dev:** bump ts-node from 9.0.0 to 9.1.1 in /frontend ([f18666b](https://github.com/softrams/bulwark/commit/f18666b4ccf5c8550ba6e94146673bed50a0d855)) - **deps-dev:** bump typescript from 4.1.2 to 4.1.3 ([29c5db5](https://github.com/softrams/bulwark/commit/29c5db53093cc51615eacc5ce2c3999987987aac)) - **report.component:** fix lint issue ([aca3ea9](https://github.com/softrams/bulwark/commit/aca3ea918f1c772d9907e10b8c476595ecfc1976)), closes [#482](https://github.com/softrams/bulwark/issues/482) ### Docs - **testing.md:** minor changes to testing.md file ([a9c04c0](https://github.com/softrams/bulwark/commit/a9c04c0a3f5e8501dc77c7e20854c3536767f639)), closes [#509](https://github.com/softrams/bulwark/issues/509) - **testing.md:** uopdating documentation ([840f4e2](https://github.com/softrams/bulwark/commit/840f4e2f1394b096c57885611e696f7de993afac)), closes [#509](https://github.com/softrams/bulwark/issues/509) - **testing.md:** update prettier documentation ([48fbef4](https://github.com/softrams/bulwark/commit/48fbef44c60a7de22fca694ac3a2f0f85651031b)), closes [#509](https://github.com/softrams/bulwark/issues/509) ### [6.3.1](https://github.com/softrams/bulwark/compare/v6.3.0...v6.3.1) (2020-12-12) ### Others - **deps:** bump ini from 1.3.5 to 1.3.8 ([88ab044](https://github.com/softrams/bulwark/commit/88ab044a4a7a8ace0c8da6b7d7486cd4652e836c)) ## [6.3.0](https://github.com/softrams/bulwark/compare/v6.2.6...v6.3.0) (2020-12-10) ### Features - **report component:** add numbering for list ([e6d3877](https://github.com/softrams/bulwark/commit/e6d3877547c1e2287f1fa4169034994566643a9b)), closes [#478](https://github.com/softrams/bulwark/issues/478) [#478](https://github.com/softrams/bulwark/issues/478) - **report component:** add numbers to pie chart ([4b12bfb](https://github.com/softrams/bulwark/commit/4b12bfbc56d48c05ef13ddb12861a180a06a8c12)), closes [#485](https://github.com/softrams/bulwark/issues/485) - **report component:** numbered list for reports ([75f6793](https://github.com/softrams/bulwark/commit/75f67934d66c67c1cd6cf38ab048373f11b432ad)), closes [#478](https://github.com/softrams/bulwark/issues/478) [#478](https://github.com/softrams/bulwark/issues/478) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([0df647a](https://github.com/softrams/bulwark/commit/0df647ab9bdb33ad004c96eb613c6a6e9765a495)) - **deps:** bump @angular/cli from 11.0.2 to 11.0.3 in /frontend ([9f38774](https://github.com/softrams/bulwark/commit/9f38774412d2e383cd0539aca7ce13802fe8d1c6)) - **deps:** bump core-js from 3.7.0 to 3.8.1 in /frontend ([64bafae](https://github.com/softrams/bulwark/commit/64bafae6afbc50da8eaa9cb4e33c79de2e19ec0c)) - **deps:** bump ngx-markdown from 11.0.0 to 11.0.1 in /frontend ([72f0e0e](https://github.com/softrams/bulwark/commit/72f0e0e235f710998a55e5a9ddf30429780cb183)) - **deps:** bump ts-node from 9.0.0 to 9.1.0 ([8b4064c](https://github.com/softrams/bulwark/commit/8b4064c31a53501dcb13597617b7a46212674af8)) - **deps-dev:** bump @angular/language-service in /frontend ([655443d](https://github.com/softrams/bulwark/commit/655443db8fa41db52cbbcded63faef9f89f9e1e7)) - **deps-dev:** bump @types/jest from 26.0.15 to 26.0.16 ([56374f8](https://github.com/softrams/bulwark/commit/56374f82f24b82e9237ce06d09e4c3c5faeba3a9)) - **package.json:** add highlight.js v 10 to dev dependencies ([6db8153](https://github.com/softrams/bulwark/commit/6db815387426aa26a9fa5b49001c9f2f6d6f4973)) - **package.json:** remove yargs package ([b17ec21](https://github.com/softrams/bulwark/commit/b17ec21f3637364c0d474e63204ab5cf89ccb076)) ### [6.2.6](https://github.com/softrams/bulwark/compare/v6.2.5...v6.2.6) (2020-12-02) ### Bug Fixes - **vuln form component:** fixing the markdown icon ([a7eee64](https://github.com/softrams/bulwark/commit/a7eee640297fbd76ad98aae37b66afd9c8bf5a86)), closes [#468](https://github.com/softrams/bulwark/issues/468) ### Code Refactoring - **file.utility.ts:** update max file size to 5m ([d3d1a44](https://github.com/softrams/bulwark/commit/d3d1a4408ef127317ac9daf95db1a91415a34f5d)) ### [6.2.5](https://github.com/softrams/bulwark/compare/v6.2.4...v6.2.5) (2020-12-01) ### CI - **docker updates:** docker updates ([1bd423d](https://github.com/softrams/bulwark/commit/1bd423d9700dc0ec9b5c819351ac5b54379855cd)) ### Build System - **@angular/cli migrate:** update @angular/cli to version 10 ([93cd1e2](https://github.com/softrams/bulwark/commit/93cd1e2b8bd589a09df6db52d304e8b1c97de6e1)) - **angular 11:** upgrade to Angular 11 ([d5e4ea9](https://github.com/softrams/bulwark/commit/d5e4ea9582d01e5d88827ede0d73a2558853e0e5)) - **dockerfile:** cleanup docker tech debt and refactor for quicker builds ([cc7dcf6](https://github.com/softrams/bulwark/commit/cc7dcf642a92f4e7d0785b356d779c39ec9f80d7)) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([d8e134e](https://github.com/softrams/bulwark/commit/d8e134ef70b18dae29d356be0a01449ece788c4c)) - **deps:** bump @angular/cdk from 10.2.7 to 11.0.1 in /frontend ([6161aa8](https://github.com/softrams/bulwark/commit/6161aa881b3a160a7cf775a470d05f8bff9585ce)) - **deps:** bump @angular/cli from 11.0.1 to 11.0.2 in /frontend ([32434b9](https://github.com/softrams/bulwark/commit/32434b9ac7128cca0a08380052a241d2b6bc1141)) - **deps:** bump @ng-select/ng-select from 5.0.8 to 5.0.9 in /frontend ([500c86d](https://github.com/softrams/bulwark/commit/500c86d680f586538635cb4258cbb15733afa130)) - **deps:** bump @types/express from 4.17.8 to 4.17.9 ([9cb8fef](https://github.com/softrams/bulwark/commit/9cb8fefbe69c4282cd63d8a98fb6681132291847)) - **deps:** bump ngx-markdown from 10.1.1 to 11.0.0 in /frontend ([47ac561](https://github.com/softrams/bulwark/commit/47ac56140eebff27ba37d6089247304f68d4e68c)) - **deps:** bump nodemailer from 6.4.15 to 6.4.16 ([d871df5](https://github.com/softrams/bulwark/commit/d871df5af925a136fe52728be754426777c5f504)) - **deps:** bump primeicons from 4.0.0 to 4.1.0 in /frontend ([1db7906](https://github.com/softrams/bulwark/commit/1db7906a1b2217266bbd76c92d60374733f74132)) - **deps:** bump puppeteer from 5.4.1 to 5.5.0 ([2205441](https://github.com/softrams/bulwark/commit/2205441f6de1038252a8321b3e86d87604643adb)) - **deps:** bump yargs from 16.1.0 to 16.1.1 ([63bacea](https://github.com/softrams/bulwark/commit/63baceac6270d593a6e02aad44b064060d54bfae)) - **deps-dev:** bump @angular/language-service in /frontend ([c20f6b1](https://github.com/softrams/bulwark/commit/c20f6b12d3c1a3c3b1cc43cb993b6d06595f3a3d)) - **deps-dev:** bump @babel/core from 7.12.3 to 7.12.7 ([f9f61d4](https://github.com/softrams/bulwark/commit/f9f61d4373e71a0e706e37279ac823d35547cec7)) - **deps-dev:** bump @babel/core from 7.12.7 to 7.12.9 ([2e314f1](https://github.com/softrams/bulwark/commit/2e314f12b61a07a326413f1d6f2beabeea97a7e2)) - **deps-dev:** bump @babel/preset-env from 7.12.1 to 7.12.7 ([7cfeb91](https://github.com/softrams/bulwark/commit/7cfeb916419d56c1ac0b4b97cfedb4f5eb4aba73)) - **deps-dev:** bump @babel/preset-typescript from 7.12.1 to 7.12.7 ([3090314](https://github.com/softrams/bulwark/commit/3090314675942148fb1d2332e1ea58051b6d412d)) - **deps-dev:** bump @types/jasmine from 3.6.1 to 3.6.2 in /frontend ([920851f](https://github.com/softrams/bulwark/commit/920851f62269ee5acd4392b8027c6210236df95a)) - **deps-dev:** bump @types/node from 14.14.6 to 14.14.8 in /frontend ([51e8005](https://github.com/softrams/bulwark/commit/51e8005fe67e94ac4847b2f9b153df22ba1e42c0)) - **deps-dev:** bump @types/node from 14.14.8 to 14.14.9 in /frontend ([f0d7650](https://github.com/softrams/bulwark/commit/f0d7650421bd9acc40a91a088307cc0d4cf71f5c)) - **deps-dev:** bump @types/node from 14.14.9 to 14.14.10 in /frontend ([7a835f7](https://github.com/softrams/bulwark/commit/7a835f7b0beff8612f07d2a0b71e34df72dabee0)) - **deps-dev:** bump jasmine-spec-reporter in /frontend ([ed06f8e](https://github.com/softrams/bulwark/commit/ed06f8e14cfe0c9599a52545cdb0768501a3d11f)) - **deps-dev:** bump typescript from 4.0.5 to 4.1.2 ([a5588f4](https://github.com/softrams/bulwark/commit/a5588f490a24b547146ccfd06b3fc95b30625532)) - **package.json:** fix package.json conflict ([d3ace4e](https://github.com/softrams/bulwark/commit/d3ace4e50c0c4c5299753dc833790550fc4e894d)) - **package.json:** update `npm run commit` script ([cdf1948](https://github.com/softrams/bulwark/commit/cdf1948dbb2b0df00dd02b050962809bd32f04a6)) - **stale.yml:** add stale bot configuration ([8c86b2a](https://github.com/softrams/bulwark/commit/8c86b2ae2361b5e46c44f049e9c4403cac8687bb)) ### Code Refactoring - **report component:** fix plurality of day vs days ([861469b](https://github.com/softrams/bulwark/commit/861469b318feb59f5cdc28ce2c1efeb8ed31c665)) - **report component:** grammar Changes ([f99ab88](https://github.com/softrams/bulwark/commit/f99ab887e5de54fffe1a54e5250e390ad9cb96d0)) ### Docs - **readme.md:** update README ([6441177](https://github.com/softrams/bulwark/commit/6441177020b0f866391ed77e8401420761c7bcac)) ### [6.2.4](https://github.com/softrams/bulwark/compare/v6.2.3...v6.2.4) (2020-11-11) ### Bug Fixes - **docker-run-exec.ts:** update docker-compose.yml with new npm script ([cd81ba7](https://github.com/softrams/bulwark/commit/cd81ba74f5687f168dd25cce9a4147b9ed5b3214)), closes [#434](https://github.com/softrams/bulwark/issues/434) - **update docker tag to latest:** no longer using a pinned version in docker-compse.yml ([6505c1c](https://github.com/softrams/bulwark/commit/6505c1cff2e548fa47e4b51ab06f69d5240f9439)), closes [#434](https://github.com/softrams/bulwark/issues/434) ### Others - **deps:** bump core-js from 3.6.5 to 3.7.0 in /frontend ([2ed9be8](https://github.com/softrams/bulwark/commit/2ed9be8ace1cd606525cceb18c204fd67fafd390)) - **deps:** bump nodemailer from 6.4.14 to 6.4.15 ([c476958](https://github.com/softrams/bulwark/commit/c476958b1d749c8a26b1b595ae856e2bba084cd9)) - **deps:** bump password-validator from 5.1.0 to 5.1.1 ([c97982c](https://github.com/softrams/bulwark/commit/c97982c1637961ff15c3b22775614ab858d6954d)) - **deps-dev:** bump ts-jest from 26.4.3 to 26.4.4 ([efe384b](https://github.com/softrams/bulwark/commit/efe384bf12c611a07cc41fa7d10bf45324a0761e)) ### [6.2.3](https://github.com/softrams/bulwark/compare/v6.2.2...v6.2.3) (2020-11-05) ### Docs - **docker-compose.yml:** update image version to 6.2.3 ([a90bddf](https://github.com/softrams/bulwark/commit/a90bddf2992e68f3317283cda77c5416b7c88120)) - **testing.md contributing.md:** update testing and contribution documentation ([9b84c2b](https://github.com/softrams/bulwark/commit/9b84c2b6996c31ef7130f26afd8dc4814194b077)) ### Others - **deps:** bump @angular/animations from 10.2.0 to 10.2.1 in /frontend ([52e5888](https://github.com/softrams/bulwark/commit/52e588822b139ef3e536a01a167779bb4f7b53dc)) - **deps:** bump @angular/animations from 10.2.1 to 10.2.2 in /frontend ([b0e69b8](https://github.com/softrams/bulwark/commit/b0e69b8921fbc4ec34593c9c3d7b48298f065fc0)) - **deps:** bump @angular/cdk from 10.2.6 to 10.2.7 in /frontend ([59dd1f2](https://github.com/softrams/bulwark/commit/59dd1f28584577d744ef7c36086d772c97dfee5e)) - **deps:** bump helmet from 4.1.1 to 4.2.0 ([ce2be8b](https://github.com/softrams/bulwark/commit/ce2be8bfd77ababcb800e0f91b49076f18cf1abe)) - **deps:** bump typeorm from 0.2.28 to 0.2.29 ([191c98e](https://github.com/softrams/bulwark/commit/191c98e01d0cc34ff5deccb1ca8e29dc4f46fa05)) - **deps-dev:** bump @angular/language-service in /frontend ([7c2854b](https://github.com/softrams/bulwark/commit/7c2854b34aaeb8ab40e22380e68a2f343bf80802)) - **deps-dev:** bump @angular/language-service in /frontend ([771a992](https://github.com/softrams/bulwark/commit/771a992a413c50174c4ee094eddf1191007029a0)) - **deps-dev:** bump @types/jasmine from 3.6.0 to 3.6.1 in /frontend ([d050ff0](https://github.com/softrams/bulwark/commit/d050ff0f1040061f229c8ea813bafaec261c6a95)) - **deps-dev:** bump @types/node from 14.14.5 to 14.14.6 in /frontend ([cb4e421](https://github.com/softrams/bulwark/commit/cb4e42145217270a9d8b76809998ba6f0948a90f)) - **deps-dev:** bump babel-jest from 26.6.1 to 26.6.2 ([684a36e](https://github.com/softrams/bulwark/commit/684a36edb9167027e5db8f0042a9dc43992154b9)) - **deps-dev:** bump babel-jest from 26.6.2 to 26.6.3 ([05ff020](https://github.com/softrams/bulwark/commit/05ff020a0b0dcb4b8c2f28ab466a84f66c630272)) - **deps-dev:** bump jest from 26.6.1 to 26.6.2 ([beed374](https://github.com/softrams/bulwark/commit/beed374f1510bd000b8c2366ac2606357a2ba82c)) - **deps-dev:** bump jest from 26.6.2 to 26.6.3 ([d6be217](https://github.com/softrams/bulwark/commit/d6be2173339386df7db1d6009e291da66e976558)) ### CI - **codeql-analysis.yml:** update codeql-analysis.yml ([c46b22a](https://github.com/softrams/bulwark/commit/c46b22a14973226b872adeec4a3059b427ee19ee)) - **dependabot.yml:** update dependabot.yml configuration ([3f2c01b](https://github.com/softrams/bulwark/commit/3f2c01b476141c80d4eb75d010447c007dcb6f10)) ### [6.2.2](https://github.com/softrams/bulwark/compare/v6.2.1...v6.2.2) (2020-11-02) ### Docs - **security.md:** update security policy supported version statement. Update docker-compose.yml ver ([14f6b92](https://github.com/softrams/bulwark/commit/14f6b920af6d72841df273aca6c35e5a94ced819)) ### [6.2.1](https://github.com/softrams/bulwark/compare/v6.2.0...v6.2.1) (2020-10-28) ### CI - **docker-compose.yml:** update docker container version to run Bulwark 6.2.0 ([8618557](https://github.com/softrams/bulwark/commit/86185571bce6adf51db362f2a74960cd531acc44)) ## [6.2.0](https://github.com/softrams/bulwark/compare/v6.1.0...v6.2.0) (2020-10-28) ### Features - **asset-form:** use Prime NG for inputs, buttons and password form components ([e8f8e46](https://github.com/softrams/bulwark/commit/e8f8e46439b505e2f6a0a6656f98331dff20470a)), closes [#369](https://github.com/softrams/bulwark/issues/369) - **report component:** added pie chart and radar chart to report ([d955720](https://github.com/softrams/bulwark/commit/d955720db93482dd3f12f34a37a77f4f04005776)), closes [#410](https://github.com/softrams/bulwark/issues/410) - **report component:** update report structure ([ec5d06c](https://github.com/softrams/bulwark/commit/ec5d06cc67b8471d829bc64022243bbf2fd13413)) - **user-profile:** use Prime NG for inputs and cards ([5623cb5](https://github.com/softrams/bulwark/commit/5623cb5f5b65c50037d5b8aeb0e963ff51bd4bca)), closes [#377](https://github.com/softrams/bulwark/issues/377) ### Others - **deps:** bump @angular/cdk from 10.2.5 to 10.2.6 in /frontend ([3a00265](https://github.com/softrams/bulwark/commit/3a00265dcbf215591949d9ee404e6635141ff4fd)) - **deps:** bump puppeteer from 5.3.1 to 5.4.0 ([a92cdc5](https://github.com/softrams/bulwark/commit/a92cdc52f35e87a6a788374b59a99af871eb3078)) - **deps:** bump puppeteer from 5.4.0 to 5.4.1 ([4f83bc8](https://github.com/softrams/bulwark/commit/4f83bc81fbaec0563768c8f0008b3cb50ff5f694)) - **deps-dev:** bump @types/jasmine from 3.5.14 to 3.6.0 in /frontend ([f596d72](https://github.com/softrams/bulwark/commit/f596d72f615f80e1c146798372d40da9664a9bd3)) - **deps-dev:** bump @types/node from 14.14.2 to 14.14.3 in /frontend ([1c95507](https://github.com/softrams/bulwark/commit/1c95507b506bfe423cbec697dde0231c1971f9ec)) - **deps-dev:** bump @types/node from 14.14.3 to 14.14.5 in /frontend ([3c9d0d7](https://github.com/softrams/bulwark/commit/3c9d0d732a2f4f02814efff709888d6c5a1bc95f)) - **deps-dev:** bump babel-jest from 26.6.0 to 26.6.1 ([f5325f8](https://github.com/softrams/bulwark/commit/f5325f81f03070e5ffb0f5531fabfb66c106b8af)) - **deps-dev:** bump jest from 26.6.0 to 26.6.1 ([53f64ab](https://github.com/softrams/bulwark/commit/53f64ab91e7db177afb8126471b33ca684b0033d)) - **deps-dev:** bump ts-jest from 26.4.1 to 26.4.2 ([a2be373](https://github.com/softrams/bulwark/commit/a2be37332696f9c112a7b97821f4e2f93836cc8d)) - **deps-dev:** bump ts-jest from 26.4.2 to 26.4.3 ([29d1404](https://github.com/softrams/bulwark/commit/29d14044d7ac0bf888f97f06edd1b00d567afd6b)) - **deps-dev:** bump typescript from 4.0.3 to 4.0.5 ([b16b187](https://github.com/softrams/bulwark/commit/b16b187e53d346b8917de8addae9c02ad01243af)) - **merge dev into branch:** merge develop into this branch ([9f7b32d](https://github.com/softrams/bulwark/commit/9f7b32dd635f47ed6f3834ebd8bea43bbdcbbe25)) - **package.json:** updated package.json with additional contributor name ([20d8b47](https://github.com/softrams/bulwark/commit/20d8b478bc780786c8dfb6252422337f6123d737)) ## [6.1.0](https://github.com/softrams/bulwark/compare/v6.0.6...v6.1.0) (2020-10-23) ### Features - email validate with prime ng ([b40f588](https://github.com/softrams/bulwark/commit/b40f588f36435f4a0828c55c76cd042623dd867e)) - forgot password with prime ng ([36e1d10](https://github.com/softrams/bulwark/commit/36e1d10ce841748e66cbc61e79e0daf2f173e61c)) - login with prime ng ([eca4924](https://github.com/softrams/bulwark/commit/eca49249a8d933671f6afd6d67276cc53cd34b30)) - register with prime ng ([2688138](https://github.com/softrams/bulwark/commit/2688138634dd64d5282c22e24ea1c0819a082546)) - reset password with prime ng ([b1f082c](https://github.com/softrams/bulwark/commit/b1f082cc41012d8a52736689e4401c28e78cbfbe)) - settings with prime ng ([a830258](https://github.com/softrams/bulwark/commit/a830258a79621b7ec70ab30c908ec9c8646c3e00)) ### Bug Fixes - button scss side effect ([4e0cf29](https://github.com/softrams/bulwark/commit/4e0cf298f3bc6ff43bbb72d884d21f1e0206f25e)) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([535777d](https://github.com/softrams/bulwark/commit/535777de4f6c40c17a1e321a7f580130803f69f1)) - **deps:** bump @angular/animations from 10.1.6 to 10.2.0 in /frontend ([002a996](https://github.com/softrams/bulwark/commit/002a996996f7ca00f0cf50f63fce6c6e768006b7)) - **deps:** bump @angular/cli from 10.1.7 to 10.2.0 in /frontend ([dc5a468](https://github.com/softrams/bulwark/commit/dc5a4688a3951d7d68fc350c0f51de54fa794634)) - **deps-dev:** bump @angular/language-service in /frontend ([22879fb](https://github.com/softrams/bulwark/commit/22879fb82a811b539ca6be26bee879de673fba5f)) - **deps-dev:** bump @babel/core from 7.12.1 to 7.12.3 ([9adb353](https://github.com/softrams/bulwark/commit/9adb3536fbd74dcf36b79683f0f13b1376ce6607)) - **deps-dev:** bump @types/jest from 26.0.14 to 26.0.15 ([0e3e7f0](https://github.com/softrams/bulwark/commit/0e3e7f006c4ef845ee2b252458a4cf106210653f)) - **deps-dev:** bump @types/node from 14.11.10 to 14.14.0 in /frontend ([86f9969](https://github.com/softrams/bulwark/commit/86f99697d9e72506162a9567976d2a1d91a8c1c6)) - **deps-dev:** bump @types/node from 14.11.8 to 14.11.10 in /frontend ([cb2f61c](https://github.com/softrams/bulwark/commit/cb2f61cd2e1e7ba2914c661596583182e923db3c)) - **deps-dev:** bump @types/node from 14.14.0 to 14.14.2 in /frontend ([84ae92c](https://github.com/softrams/bulwark/commit/84ae92c28921d417b2fddb735a830b6bb5d0c33f)) - **deps-dev:** bump babel-jest from 26.5.2 to 26.6.0 ([022c89d](https://github.com/softrams/bulwark/commit/022c89de15c2d526e33cf60b4fc1ee14789aa094)) - **deps-dev:** bump jest from 26.5.3 to 26.6.0 ([81aea5a](https://github.com/softrams/bulwark/commit/81aea5a5334f77e46a533ab3a7777a51450bdc2c)) ### [6.0.6](https://github.com/softrams/bulwark/compare/v6.0.5...v6.0.6) (2020-10-21) ### Bug Fixes - **vuln-form component:** fixed missing delete screenshot icon ([60be8a9](https://github.com/softrams/bulwark/commit/60be8a9f58fa79885384f17d459121dd41c058fa)) ### [6.0.5](https://github.com/softrams/bulwark/compare/v6.0.4...v6.0.5) (2020-10-16) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([3ce0d49](https://github.com/softrams/bulwark/commit/3ce0d49bc84ceaee825b22e917e7328e3c8bb8f0)) - **deps:** bump @angular/animations from 10.1.5 to 10.1.6 in /frontend ([1eea3d3](https://github.com/softrams/bulwark/commit/1eea3d375b816d6bcc69f3a8285124758af2fd47)) - **deps:** bump @angular/cdk from 10.2.4 to 10.2.5 in /frontend ([ffeacb9](https://github.com/softrams/bulwark/commit/ffeacb910c2fd7fb281cd9798221c923177eca93)) - **deps:** bump @angular/cli from 10.1.6 to 10.1.7 in /frontend ([8af8735](https://github.com/softrams/bulwark/commit/8af87353f97799dfbe91aeec013375212e5953f7)) - **deps:** bump nodemailer from 6.4.13 to 6.4.14 ([0f0201f](https://github.com/softrams/bulwark/commit/0f0201f491ca69fddad912b492e3a727911ec41a)) - **deps:** bump yargs from 16.0.3 to 16.1.0 ([541bbf3](https://github.com/softrams/bulwark/commit/541bbf32b3a02df6655901857d1502070fe40585)) - **deps-dev:** bump @angular/language-service in /frontend ([94914f5](https://github.com/softrams/bulwark/commit/94914f52c55d69dc48144c0ce5105687e08788b6)) - **deps-dev:** bump @babel/core from 7.11.6 to 7.12.0 ([0538836](https://github.com/softrams/bulwark/commit/0538836658c07c5854f9d87fe77de61eaac273c8)) - **deps-dev:** bump @babel/core from 7.12.0 to 7.12.1 ([55c6f86](https://github.com/softrams/bulwark/commit/55c6f86613aec80204ce1f2b1d01b1bef4943503)) - **deps-dev:** bump @babel/preset-env from 7.11.5 to 7.12.0 ([4a3c1c6](https://github.com/softrams/bulwark/commit/4a3c1c61ff6cca5118d289994196b0519a09d07f)) - **deps-dev:** bump @babel/preset-env from 7.12.0 to 7.12.1 ([def2233](https://github.com/softrams/bulwark/commit/def2233f2c8c76e7174f68cebc9a1a0c1e621b91)) - **deps-dev:** bump @babel/preset-typescript from 7.10.4 to 7.12.0 ([2a2861b](https://github.com/softrams/bulwark/commit/2a2861b6407b5df398481eead368578ee5edf3b0)) - **deps-dev:** bump @babel/preset-typescript from 7.12.0 to 7.12.1 ([a3ed773](https://github.com/softrams/bulwark/commit/a3ed7738028ff4ea26b52a1762291c649ec24a57)) ### [6.0.4](https://github.com/softrams/bulwark/compare/v6.0.3...v6.0.4) (2020-10-14) ### Docs - **readme.md:** updated README.md ([0966f9f](https://github.com/softrams/bulwark/commit/0966f9f6b0a35fb6397b083dda8ed222802136ed)) ### [6.0.3](https://github.com/softrams/bulwark/compare/v6.0.2...v6.0.3) (2020-10-13) ### Others - **release:** 6.0.1 ([0fbf5c2](https://github.com/softrams/bulwark/commit/0fbf5c2b77e3cb045350d9eca757e26fbbc48377)) - **release:** 6.0.3 ([4a186ba](https://github.com/softrams/bulwark/commit/4a186ba061cdf00480d0cb9c7e6bb51b977ed47e)) - **release:** 6.0.3 ([b296c2b](https://github.com/softrams/bulwark/commit/b296c2b7529b8047cda3a5bbe14a74ceca996986)) - **release:** 6.0.5 ([6f50575](https://github.com/softrams/bulwark/commit/6f505752302ed7a539fc7b8cabe4807e4f21e662)) ### Docs - **updated readme:** updated readme ([d9b19b5](https://github.com/softrams/bulwark/commit/d9b19b508fa705e94a5cd6cd26625ae7673c8cee)) ### [6.0.1](https://github.com/softrams/bulwark/compare/v6.0.2...v6.0.1) (2020-10-13) ### Others - **release:** 6.0.3 ([b296c2b](https://github.com/softrams/bulwark/commit/b296c2b7529b8047cda3a5bbe14a74ceca996986)) ## [6.0.0](https://github.com/softrams/bulwark/compare/v5.0.0...v6.0.0) (2020-10-13) ### ⚠ BREAKING CHANGES - **add user entity column for newemail:** added new newEmail column to the user entity - **organization and file table:** Updates to the schema. Removed relationship between Organization and File. ### Features - **add user entity column for newemail:** update user entity with newEmail column ([4aef2fe](https://github.com/softrams/bulwark/commit/4aef2febad142a514529071772c35b7f8fd8c1e2)), closes [#49](https://github.com/softrams/bulwark/issues/49) - **assessments:** add custom table filter matchMode to filter Testers array ([0e555b4](https://github.com/softrams/bulwark/commit/0e555b4041657c1abf1d919298cb3beef8259ca2)) - **create initial seed user:** update initial startup to create default user ([61ddc2a](https://github.com/softrams/bulwark/commit/61ddc2a07b002e5dc53bcf812c126dbe874e707d)), closes [#322](https://github.com/softrams/bulwark/issues/322) - **dashboard, organization, vulnerability summary tables:** updated column name and headers ([bd4feac](https://github.com/softrams/bulwark/commit/bd4feacd596bdd78c61eeef0d2968a140e5c7d5f)), closes [#321](https://github.com/softrams/bulwark/issues/321) - **docker:** updating Bulwark to run from docker ([b983c52](https://github.com/softrams/bulwark/commit/b983c52db76031eacbcd452b58467d1947fd5935)), closes [#245](https://github.com/softrams/bulwark/issues/245) - **dockerfile:** dockerize Bulwark ([fe07262](https://github.com/softrams/bulwark/commit/fe07262fc9f1a33d3f05ec0b801c8b83e0abf4f8)) - **dockerfile:** dockerize Bulwark ([8ac6fdd](https://github.com/softrams/bulwark/commit/8ac6fdd3baa86cbb35425c193eb40c5861079ea7)) - **email-validate component:** created the email-validate.component.ts file ([5c1dd87](https://github.com/softrams/bulwark/commit/5c1dd87b602de9f3fd3a97ecd1db8cbf75cd6378)), closes [#49](https://github.com/softrams/bulwark/issues/49) - **new api for revoke and validate email request:** new api's with unit tests ([152cbf1](https://github.com/softrams/bulwark/commit/152cbf15961733ee131a36f20e361588561a723c)), closes [#49](https://github.com/softrams/bulwark/issues/49) - **organization and file table:** removed relation between Org and File ([05d42a0](https://github.com/softrams/bulwark/commit/05d42a0efb2cdf899e94d11b576450a282af35f1)), closes [#321](https://github.com/softrams/bulwark/issues/321) - **user controller:** created API for email update ([bcbab8f](https://github.com/softrams/bulwark/commit/bcbab8f36ddde18cfc0024ad571a5dad2dffcb40)), closes [#49](https://github.com/softrams/bulwark/issues/49) - docker ([62ee1ad](https://github.com/softrams/bulwark/commit/62ee1ad858dbb0f202976b6a4b37fc00f5ec141d)) - docker ([afd7274](https://github.com/softrams/bulwark/commit/afd72741f4e4e3024d2ed750027e30ce68b8c0e3)) - docker ([a590ecf](https://github.com/softrams/bulwark/commit/a590ecfefddeacbeb894f7c4b7524ab52191a094)) - dockerize Bulwark ([607db0b](https://github.com/softrams/bulwark/commit/607db0b722bf7597a139cb187f1f96bf32ef6b54)) ### Bug Fixes - **removed migration directory files:** removed migrations ([783ce03](https://github.com/softrams/bulwark/commit/783ce03aa30163fa8828e7b17a36151621c2b3e7)) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([5b2edba](https://github.com/softrams/bulwark/commit/5b2edba0de6ac9d6082e759b0e5057790ac9c17f)) - **deps:** bump @angular-devkit/build-angular in /frontend ([af92bac](https://github.com/softrams/bulwark/commit/af92bac398fcd8e68f109b1c360cd0442cc3ce3f)) - **deps:** bump @angular/animations from 10.1.4 to 10.1.5 in /frontend ([6f57b2e](https://github.com/softrams/bulwark/commit/6f57b2e9a2871b76f50278c34cfa633b12442740)) - **deps:** bump @angular/cdk from 10.2.3 to 10.2.4 in /frontend ([40e75d9](https://github.com/softrams/bulwark/commit/40e75d9d2efd88e9acc0f6ff43eba290cd671d2c)) - **deps:** bump @angular/cli from 10.1.4 to 10.1.5 in /frontend ([58ff160](https://github.com/softrams/bulwark/commit/58ff160bb3fe70e3a7a68567fd5d6655872b9ef6)) - **deps:** bump @angular/cli from 10.1.5 to 10.1.6 in /frontend ([e573f3d](https://github.com/softrams/bulwark/commit/e573f3de8b22b4fcb9eecfd69e3466d03a009035)) - **deps:** bump @ng-select/ng-select from 5.0.3 to 5.0.4 in /frontend ([dbcc884](https://github.com/softrams/bulwark/commit/dbcc884a8886591955f992153cd1edee8e8b92b3)) - **deps:** bump @ng-select/ng-select from 5.0.4 to 5.0.5 in /frontend ([05c1a8f](https://github.com/softrams/bulwark/commit/05c1a8f66a02878944410b8bc44f630d2f664a15)) - **deps:** bump @ng-select/ng-select from 5.0.5 to 5.0.6 in /frontend ([c73d17f](https://github.com/softrams/bulwark/commit/c73d17f609f5c202e56b5de81ce689115374a40f)) - **deps:** bump @ng-select/ng-select from 5.0.6 to 5.0.7 in /frontend ([9731080](https://github.com/softrams/bulwark/commit/9731080df713b0867c2da9689442a0b57156b486)) - **deps:** bump @ng-select/ng-select from 5.0.7 to 5.0.8 in /frontend ([47cb485](https://github.com/softrams/bulwark/commit/47cb485743cc77845a91cd934f61b25c0a53dd30)) - **deps:** bump jira-client from 6.21.0 to 6.21.1 ([25a5e8b](https://github.com/softrams/bulwark/commit/25a5e8b038dca63957e06ad8c50bbb0a360ac73e)) - **deps:** bump nodemailer from 6.4.12 to 6.4.13 ([6b389bd](https://github.com/softrams/bulwark/commit/6b389bd4d260c6f10e555957b73b262ac4447fc6)) - **deps:** bump primeng from 10.0.2 to 10.0.3 in /frontend ([015f652](https://github.com/softrams/bulwark/commit/015f652c6a4fa8d41bcf37517c1448e6f6f9d853)) - **deps:** bump tslib from 2.0.1 to 2.0.2 in /frontend ([17a8e32](https://github.com/softrams/bulwark/commit/17a8e32676bfc9167742bf058b02912af0d6d43b)) - **deps:** bump tslib from 2.0.2 to 2.0.3 in /frontend ([9ef5373](https://github.com/softrams/bulwark/commit/9ef53738e894395f5d13ff63b7589242c3f1cde7)) - **deps:** bump uuid from 8.3.0 to 8.3.1 ([9bbdd11](https://github.com/softrams/bulwark/commit/9bbdd11005095e4b43e2eda174461348089b54d2)) - **deps-dev:** bump @angular/language-service in /frontend ([9c3a1be](https://github.com/softrams/bulwark/commit/9c3a1be35226761e7472daa7e20f06bd5595da32)) - **deps-dev:** bump @types/node from 14.11.2 to 14.11.5 in /frontend ([ba63886](https://github.com/softrams/bulwark/commit/ba638868ce66ee1e7fb9e603dd54e6a497bf78f2)) - **deps-dev:** bump @types/node from 14.11.5 to 14.11.7 in /frontend ([0034d3f](https://github.com/softrams/bulwark/commit/0034d3ffd5ce95bf5d28eef68972220fe4ccd6f6)) - **deps-dev:** bump @types/node from 14.11.7 to 14.11.8 in /frontend ([534b6c8](https://github.com/softrams/bulwark/commit/534b6c8e54792b56d7f0c3182fd84b4460ac204f)) - **deps-dev:** bump babel-jest from 26.3.0 to 26.5.2 ([2c0e335](https://github.com/softrams/bulwark/commit/2c0e335612b21dba273d71c0d8251ab33a9298f7)) - **deps-dev:** bump jest from 26.4.2 to 26.5.0 ([fab2537](https://github.com/softrams/bulwark/commit/fab2537628b3ff3a2d0e4743b7db010aef14f99c)) - **deps-dev:** bump jest from 26.5.0 to 26.5.2 ([d44551b](https://github.com/softrams/bulwark/commit/d44551bc329d9f500525fa5b101b0ebe98d37ff3)) - **deps-dev:** bump jest from 26.5.2 to 26.5.3 ([4e31812](https://github.com/softrams/bulwark/commit/4e31812201e88ea8c4a11528d628f3756bc5b1eb)) ### Code Refactoring - **user.ts:** updated user column uuid to always be nullable ([79cd34e](https://github.com/softrams/bulwark/commit/79cd34efa7c15045f6e6f70a212919da1f19d082)) ## [5.0.0](https://github.com/softrams/bulwark/compare/v4.0.7...v5.0.0) (2020-10-02) ### ⚠ BREAKING CHANGES - **settings component:** Added new table for app configurations. updated API's referencing the email service to check for email configuration from database. Removed env vars for email configuration and company name. ### Features - **assessment/asset summary table:** added filtering to tables ([66b2420](https://github.com/softrams/bulwark/commit/66b242008725c826d094a78a67ea9dae270935e7)), closes [#302](https://github.com/softrams/bulwark/issues/302) - **initial commit for primeng:** initial commit for PrimeNG ([988dc2a](https://github.com/softrams/bulwark/commit/988dc2a856a1851b138aadb59d5aaaf6e8d8e6e8)), closes [#302](https://github.com/softrams/bulwark/issues/302) - **initial commit of asset and assessment primeng refactor:** initial commit of asset and assessent ([493a3d2](https://github.com/softrams/bulwark/commit/493a3d2c660363bbda6a28650b4e6766d57e6fd1)), closes [#302](https://github.com/softrams/bulwark/issues/302) - **primeng refactor:** refactored existing tables to use primeng ([0adc220](https://github.com/softrams/bulwark/commit/0adc220196e06c93b052f327a2163085caa5c8f3)), closes [#302](https://github.com/softrams/bulwark/issues/302) - **removed tester multiselect:** temporarily replaced Tester multiselect with sort ([aa332da](https://github.com/softrams/bulwark/commit/aa332dad1d258370b48e03c8a280b7eafc555afd)), closes [#302](https://github.com/softrams/bulwark/issues/302) - **settings component:** moved email configuration from env var to application ([778670e](https://github.com/softrams/bulwark/commit/778670e45a75a7c11bdacfd8674a3fa8feb60183)), closes [#260](https://github.com/softrams/bulwark/issues/260) - **update primeng table and loading spinner:** updated primeng tables. Refactored loading spinner ([c169e99](https://github.com/softrams/bulwark/commit/c169e999708f49687fa0e9fca6d457f8b790fc00)), closes [#302](https://github.com/softrams/bulwark/issues/302) ### Others - **ci:** added support for issues ([dadee4e](https://github.com/softrams/bulwark/commit/dadee4e66d5957ac3c8d7e6fe92841ac6ae49eae)) - **deps:** bump @angular-devkit/build-angular in /frontend ([3189224](https://github.com/softrams/bulwark/commit/3189224a5b21f2c26e1d97ab075379179d0465dd)) - **deps:** bump @angular-devkit/build-angular in /frontend ([ec14201](https://github.com/softrams/bulwark/commit/ec1420139a6d4d60dec10a2282697dd2ac710e68)) - **deps:** bump @angular-devkit/build-angular in /frontend ([77ac88c](https://github.com/softrams/bulwark/commit/77ac88c599ccdb630ea4f85c9f9402510754f867)) - **deps:** bump @angular/animations in /frontend ([b74a9c9](https://github.com/softrams/bulwark/commit/b74a9c9a20cf9b9c82efbea8d509534b09a30a13)) - **deps:** bump @angular/cdk from 10.2.2 to 10.2.3 in /frontend ([3d0e38d](https://github.com/softrams/bulwark/commit/3d0e38d1f2b572834427e411e2eaecd1fdcc0eac)) - **deps:** bump @angular/cli from 10.1.1 to 10.1.2 in /frontend ([45cfe34](https://github.com/softrams/bulwark/commit/45cfe34935379f7011829f85bf2eb622b978a6ea)) - **deps:** bump @angular/cli from 10.1.2 to 10.1.3 in /frontend ([1369d3c](https://github.com/softrams/bulwark/commit/1369d3c8e10ce474f3fa302e434d72663456c438)) - **deps:** bump @angular/cli from 10.1.3 to 10.1.4 in /frontend ([5a0536e](https://github.com/softrams/bulwark/commit/5a0536e1fa2f7df2da94ee66ae3b5faa2a38465c)) - **deps:** bump @fortawesome/fontawesome-svg-core in /frontend ([1a54c95](https://github.com/softrams/bulwark/commit/1a54c95e1c0ccc157441e831bf00f9bbc541a253)) - **deps:** bump @fortawesome/free-brands-svg-icons in /frontend ([830a15c](https://github.com/softrams/bulwark/commit/830a15c0b1a38dfd43198d61475aa558f6e35608)) - **deps:** bump @fortawesome/free-solid-svg-icons in /frontend ([39bd027](https://github.com/softrams/bulwark/commit/39bd027aff2457bbd3ef695151a14ffe9ca63d04)) - **deps:** bump @ng-select/ng-select from 5.0.1 to 5.0.3 in /frontend ([797caab](https://github.com/softrams/bulwark/commit/797caab204417f23519b3b4826f4405fe74b9358)) - **deps:** bump nodemailer from 6.4.11 to 6.4.12 ([a690271](https://github.com/softrams/bulwark/commit/a690271d38061c1d5b2826e10a2c3b5c59b7d96f)) - **deps:** bump primeng from 10.0.0 to 10.0.2 in /frontend ([a9e5b30](https://github.com/softrams/bulwark/commit/a9e5b30b391602a7ab3a2d044881c20c6e59f8e0)) - **deps:** bump puppeteer from 5.3.0 to 5.3.1 ([6e77d99](https://github.com/softrams/bulwark/commit/6e77d9948489da1fa7a8a16b24025f6945655d34)) - **deps:** bump typeorm from 0.2.26 to 0.2.28 ([e723343](https://github.com/softrams/bulwark/commit/e7233435811adbc5fc82ea351c10e22c8fa69e75)) - **deps-dev:** bump @angular/language-service in /frontend ([7502ade](https://github.com/softrams/bulwark/commit/7502ade9c6e5d1da72a40842236a8c48fd5a4b65)) - **deps-dev:** bump @angular/language-service in /frontend ([f20a01b](https://github.com/softrams/bulwark/commit/f20a01b67d0b6bc1323c2af3a7a20455afb1e4f8)) - **deps-dev:** bump @angular/language-service in /frontend ([856bd1c](https://github.com/softrams/bulwark/commit/856bd1c0dca011ca58c69d0e9682da83532a483e)) - **deps-dev:** bump @types/jest from 26.0.13 to 26.0.14 ([bcd296a](https://github.com/softrams/bulwark/commit/bcd296afb3ae320cbbfb7fdf5ebb9950844d03f5)) - **deps-dev:** bump @types/node from 14.10.1 to 14.10.2 in /frontend ([0384b4e](https://github.com/softrams/bulwark/commit/0384b4efd1a735b6595997742ff90c4bb026eefb)) - **deps-dev:** bump @types/node from 14.10.2 to 14.10.3 in /frontend ([d2bbdea](https://github.com/softrams/bulwark/commit/d2bbdea6d567a1d4da2f6f4572f791f829f1d356)) - **deps-dev:** bump @types/node from 14.10.3 to 14.11.1 in /frontend ([1f00fbf](https://github.com/softrams/bulwark/commit/1f00fbfd82e4205f76458f89e3c73e2876d824ad)) - **deps-dev:** bump @types/node from 14.11.1 to 14.11.2 in /frontend ([eadd88c](https://github.com/softrams/bulwark/commit/eadd88c66bd3a1842aa25b9f1a393faa295d47a6)) - **deps-dev:** bump codelyzer from 6.0.0 to 6.0.1 in /frontend ([ee12133](https://github.com/softrams/bulwark/commit/ee121334659fd3ef46db880df9581ad22affafb3)) - **deps-dev:** bump jasmine-spec-reporter in /frontend ([002fb85](https://github.com/softrams/bulwark/commit/002fb85f7670b166df47418d4ed2db25152dd64e)) - **deps-dev:** bump karma from 5.2.2 to 5.2.3 in /frontend ([9380ed0](https://github.com/softrams/bulwark/commit/9380ed0e4b6b6bf78b7c6cd16866ab5c743c3793)) - **deps-dev:** bump ts-jest from 26.3.0 to 26.4.0 ([1da83db](https://github.com/softrams/bulwark/commit/1da83db85b49bf251351019383c39f1e8fda3449)) - **deps-dev:** bump ts-jest from 26.4.0 to 26.4.1 ([6304980](https://github.com/softrams/bulwark/commit/6304980cf4390b09ee4d88c4d2c0551ac4b8f7fe)) - **deps-dev:** bump typescript from 4.0.2 to 4.0.3 ([e76deae](https://github.com/softrams/bulwark/commit/e76deaeead62bfd4d3441fbc2c66f8c33cb80c7e)) - **fixing merge conflicts:** fixed merge conflicts ([ff7e2af](https://github.com/softrams/bulwark/commit/ff7e2af483ab1a6872e002d0aa3b8f98f4f3066f)), closes [#302](https://github.com/softrams/bulwark/issues/302) ### CI - **node:** added ability to create env file before running tests ([0dc9757](https://github.com/softrams/bulwark/commit/0dc9757c66ff1a9dc2fec8101da1bd5ae4fe8b2b)) - **node:** added support for backend tests to run on ci ([e31c488](https://github.com/softrams/bulwark/commit/e31c488ee23b21399792d0f0b81a7bb4187433ed)) - **node:** added support to ci to only run if the backend files have changed ([8071cb3](https://github.com/softrams/bulwark/commit/8071cb3016c80f31ba0f14aece10093c891d8622)) - **node:** removed double qutoes ([20d1586](https://github.com/softrams/bulwark/commit/20d15866de030cef7c93c6ebffb51ebbb4c09c3e)) - **node:** removed path filter and set tests to run each time ([76d01f3](https://github.com/softrams/bulwark/commit/76d01f332fc02266ae3954f954fca40588e5f2e0)) - **user.controller:** updated user import for failed tested ([b56a7fa](https://github.com/softrams/bulwark/commit/b56a7fae4b080da1ff7528c2235d7a829ea9e9f6)) ### [4.0.7](https://github.com/softrams/bulwark/compare/v4.0.6...v4.0.7) (2020-10-01) ### [4.0.6](https://github.com/softrams/bulwark/compare/v4.0.5...v4.0.6) (2020-09-14) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([0fa83d3](https://github.com/softrams/bulwark/commit/0fa83d3ca411e2537fcca9de3445a8b35db942dd)) - **deps:** bump @angular/cli from 10.1.0 to 10.1.1 in /frontend ([edde37f](https://github.com/softrams/bulwark/commit/edde37f79999ea0c35b51629321edd214e109241)) - **deps:** bump helmet from 4.1.0 to 4.1.1 ([8d641e0](https://github.com/softrams/bulwark/commit/8d641e05c080051186cd23ac28d0a1b08039b675)) - **deps:** bump jira-client from 6.20.0 to 6.21.0 ([1b22837](https://github.com/softrams/bulwark/commit/1b22837983852429040029ceb9f98c14e5794e02)) - **deps:** bump node-fetch from 2.6.0 to 2.6.1 ([0875c69](https://github.com/softrams/bulwark/commit/0875c69e2ab4173766642086dd44e5df0cfc2634)) - **deps:** bump puppeteer from 5.2.1 to 5.3.0 ([52431ad](https://github.com/softrams/bulwark/commit/52431ad6b33bcd434e424d67f1473c47600a7765)) - **deps:** bump rxjs from 6.6.2 to 6.6.3 in /frontend ([faf866d](https://github.com/softrams/bulwark/commit/faf866dd8d481c8e47bdf4355ceccba8475afcc8)) - **deps:** bump typeorm from 0.2.25 to 0.2.26 ([0a445e1](https://github.com/softrams/bulwark/commit/0a445e1655015c723df253c5f77841a962beb366)) - **deps:** bump yargs from 15.4.1 to 16.0.0 ([6b53359](https://github.com/softrams/bulwark/commit/6b53359ba3072d8f804296bfa5f1ea651048ee99)) - **deps:** bump yargs from 16.0.0 to 16.0.3 ([9a3ed79](https://github.com/softrams/bulwark/commit/9a3ed79ed8df3a6c971c6f5e2e890cebc7f95c31)) - **deps-dev:** bump @angular/language-service in /frontend ([b2a2943](https://github.com/softrams/bulwark/commit/b2a2943533ce033392a244f1d63c15b316c059a4)) - **deps-dev:** bump @babel/core from 7.11.5 to 7.11.6 ([c79fd88](https://github.com/softrams/bulwark/commit/c79fd8866710f5a5643d80ba2caf170e327d100a)) - **deps-dev:** bump @commitlint/cli from 9.1.2 to 11.0.0 ([1abe52f](https://github.com/softrams/bulwark/commit/1abe52f9a59484885e50a273f84398a5df9e830e)) - **deps-dev:** bump @commitlint/config-conventional ([6806a6d](https://github.com/softrams/bulwark/commit/6806a6d2248dd81c42752763895eefd94d921190)) - **deps-dev:** bump @types/node from 14.10.0 to 14.10.1 in /frontend ([60e52fe](https://github.com/softrams/bulwark/commit/60e52feabab1fb8a0fc93f0c931a3e18f7f6f2c9)) - **deps-dev:** bump @types/node from 14.6.3 to 14.6.4 in /frontend ([2f86132](https://github.com/softrams/bulwark/commit/2f861320b4a92a80b7869a4db75e8d0cfc9795ba)) - **deps-dev:** bump @types/node from 14.6.4 to 14.10.0 in /frontend ([250777c](https://github.com/softrams/bulwark/commit/250777c9f126061edf24e3f315754d2e26051a3f)) - **deps-dev:** bump husky from 4.2.5 to 4.3.0 ([ddaf553](https://github.com/softrams/bulwark/commit/ddaf553bd0883c4bc06cc0d8eb76e250affd8110)) - **deps-dev:** bump karma from 5.2.1 to 5.2.2 in /frontend ([ab6e48b](https://github.com/softrams/bulwark/commit/ab6e48b4756964b971b8d609cdb6e25f16a3db33)) ### [4.0.5](https://github.com/softrams/bulwark/compare/v4.0.4...v4.0.5) (2020-09-03) ### Bug Fixes - **ormconfig.js:** updated ormconfig so that it allows to safely run migrations ([cd52a9c](https://github.com/softrams/bulwark/commit/cd52a9cf8a8b06549f0e2e05f6e5931be10127a3)), closes [#262](https://github.com/softrams/bulwark/issues/262) - **report generation:** updated puppeteer report generation so that it outputs to the `temp` folder ([38f7bcf](https://github.com/softrams/bulwark/commit/38f7bcf4e2bb4d390a5df65de119af92d577de7a)), closes [#259](https://github.com/softrams/bulwark/issues/259) ### Others - **deps:** bump @angular/cli from 10.0.8 to 10.1.0 in /frontend ([cc699f3](https://github.com/softrams/bulwark/commit/cc699f3b68e69bfc8249cb86609bd50c758529c0)) - **deps:** bump @types/express from 4.17.7 to 4.17.8 ([8a5726b](https://github.com/softrams/bulwark/commit/8a5726ba6a2142e44a5a732dc7e9ff04424da595)) - **deps-dev:** bump @angular/language-service in /frontend ([d3d9580](https://github.com/softrams/bulwark/commit/d3d9580267e46f9b897d1a52c4548ef37ce8b7fd)) - **deps-dev:** bump @babel/core from 7.11.4 to 7.11.5 ([719c25c](https://github.com/softrams/bulwark/commit/719c25ced2d4361ece72bad730c27077e6531b23)) - **deps-dev:** bump @babel/preset-env from 7.11.0 to 7.11.5 ([ce52573](https://github.com/softrams/bulwark/commit/ce525733276515de64e73994d3ff2bdf2d73d503)) - **deps-dev:** bump @types/jest from 26.0.10 to 26.0.12 ([0fc4012](https://github.com/softrams/bulwark/commit/0fc401251a422d729af393a5df97a36cf24d8932)) - **deps-dev:** bump @types/jest from 26.0.12 to 26.0.13 ([5d7e519](https://github.com/softrams/bulwark/commit/5d7e5193e2188ca7741a6d72c09d491c0f6b1171)) - **deps-dev:** bump @types/node from 14.6.2 to 14.6.3 in /frontend ([c62143a](https://github.com/softrams/bulwark/commit/c62143a020afb9c9811aa8ec5dde05c49c48f0a2)) - **deps-dev:** bump karma from 5.1.1 to 5.2.0 in /frontend ([f372b0f](https://github.com/softrams/bulwark/commit/f372b0f7eb5735268fa2f944fe7be55efb227554)) - **deps-dev:** bump karma from 5.2.0 to 5.2.1 in /frontend ([6524c98](https://github.com/softrams/bulwark/commit/6524c98445c142f20fe04fc31d348d30d7ef85cf)) ### [4.0.4](https://github.com/softrams/bulwark/compare/v4.0.3...v4.0.4) (2020-08-31) ### Bug Fixes - **add temp folder to src:** add `temp` folder to fix Jira screenshot attachments ([746336f](https://github.com/softrams/bulwark/commit/746336fda2d5fc9dfeee3e4ab4d569f251aec8ab)), closes [#248](https://github.com/softrams/bulwark/issues/248) ### [4.0.3](https://github.com/softrams/bulwark/compare/v4.0.2...v4.0.3) (2020-08-31) ### Bug Fixes - **column-mapper.utility.ts:** fixed typescript build break. made column mapper a string for In ([d2ef14c](https://github.com/softrams/bulwark/commit/d2ef14cb333e89807c8fa92dc1f89303f65b19ad)) ### Others - **deps:** bump @fortawesome/free-solid-svg-icons in /frontend ([10ee5fd](https://github.com/softrams/bulwark/commit/10ee5fdb0480463ac101d75c4045d784b1b90387)) - **deps-dev:** bump @types/node from 14.6.0 to 14.6.1 in /frontend ([e31dfa0](https://github.com/softrams/bulwark/commit/e31dfa0458d349bc97e6822def324e568094cbb6)) - **deps-dev:** bump @types/node from 14.6.1 to 14.6.2 in /frontend ([b428221](https://github.com/softrams/bulwark/commit/b42822197284d97455825a3d0af2b9489529987d)) - **deps-dev:** bump typescript from 3.9.7 to 4.0.2 ([10a17a6](https://github.com/softrams/bulwark/commit/10a17a665f60c314bcd21732b18b6c2278c319b8)) ### [4.0.2](https://github.com/softrams/bulwark/compare/v4.0.1...v4.0.2) (2020-08-27) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([bae4ca9](https://github.com/softrams/bulwark/commit/bae4ca9dc29a26aee767519b0d2eda17a356a89b)) - **deps:** bump @angular/cli from 10.0.7 to 10.0.8 in /frontend ([52dd36b](https://github.com/softrams/bulwark/commit/52dd36ba2c83056cdd6b4b936792ca2ce147ba4b)) - **deps:** bump jira-client from 6.19.0 to 6.20.0 ([014bd86](https://github.com/softrams/bulwark/commit/014bd868c0a25bd2be62a21f8fd9ba98a344eea3)) - **deps-dev:** bump @angular/language-service in /frontend ([09938ab](https://github.com/softrams/bulwark/commit/09938abfa8087936cc7b08abae8802020bde8453)) - **deps-dev:** bump @angular/language-service in /frontend ([94fc0c9](https://github.com/softrams/bulwark/commit/94fc0c95183067e7ca7dd90e5ec8073050b1b186)) - **deps-dev:** bump @types/jasmine from 3.5.13 to 3.5.14 in /frontend ([5b9aa1d](https://github.com/softrams/bulwark/commit/5b9aa1d9f7a455b3d5d4fe6e7a97dfccf5a08565)) - **deps-dev:** bump cz-conventional-changelog from 3.2.0 to 3.2.1 ([492dc6b](https://github.com/softrams/bulwark/commit/492dc6b73c432ea2c6204e37a2611f20a1c6de3f)) - **deps-dev:** bump cz-conventional-changelog from 3.2.1 to 3.3.0 ([7f22e05](https://github.com/softrams/bulwark/commit/7f22e05fc2e001f2250bd938db6c2f244ff8067f)) - **deps-dev:** bump ts-jest from 26.2.0 to 26.3.0 ([5d9e103](https://github.com/softrams/bulwark/commit/5d9e1037896ac023a016be8ff2b7e5b073100a52)) - **deps-dev:** bump ts-node from 8.10.2 to 9.0.0 in /frontend ([9a31591](https://github.com/softrams/bulwark/commit/9a31591afa9e47390c3a1e5b6c63535f6596ddf7)) ### [4.0.1](https://github.com/softrams/bulwark/compare/v4.0.0...v4.0.1) (2020-08-24) ### Bug Fixes - **asset and vuln:** fixed update Asset and fixed get Vulnerability by id ([ea4d4f1](https://github.com/softrams/bulwark/commit/ea4d4f1cdb17e52e617acaa1642eeae581b15e17)) ## [4.0.0](https://github.com/softrams/bulwark/compare/v3.2.3...v4.0.0) (2020-08-24) ### ⚠ BREAKING CHANGES - **create jira table:** Added JIRA table - **jira utilities:** Breaking changes include updates to the create and update Asset APIs to include new JIRA properties - **asset controller:** Updated asset model with jira properties: username, host, api key. Validation broken. Tests broken. - **column mapper:** Updated column options - **asset archive:** Additional asset column breaks current model validations. ### Features - **assessment controller:** assessment deletion feature ([b96b977](https://github.com/softrams/bulwark/commit/b96b9771d6df91b4c5cc78bec5cd1c6ad2281472)), closes [#177](https://github.com/softrams/bulwark/issues/177) - **assessment tester column:** updated API to get assessments by ID to include testers ([a5e4968](https://github.com/softrams/bulwark/commit/a5e4968aef8a57245b9990499e13ff0dc095ffc1)), closes [#205](https://github.com/softrams/bulwark/issues/205) - **asset archive:** implement ability to archive assets ([6e56a43](https://github.com/softrams/bulwark/commit/6e56a43d593a7062096e4895d0a8a98c53066b3e)), closes [#167](https://github.com/softrams/bulwark/issues/167) - **asset controller:** updated Asset model with jira properties ([8d4fa08](https://github.com/softrams/bulwark/commit/8d4fa08cdacd400acba47266e13b9f0cbf940fca)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **column mapper:** create dynamic column options ([05fdac1](https://github.com/softrams/bulwark/commit/05fdac12ee01e1ce1b0e7b6eaa204a6bc1976f39)), closes [#167](https://github.com/softrams/bulwark/issues/167) - **create jira table:** broke apart Asset table and moved JIRA information to JIRA table ([7d31d45](https://github.com/softrams/bulwark/commit/7d31d4576cce96c5e99d8cf7b5e8e9712c3458d0)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **jira:** initial commit for JIRA src ([acc78ab](https://github.com/softrams/bulwark/commit/acc78ab6c9fca139aa9b5ffe3b57d68d8d18dfff)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **jira issue save update with refactored code:** updated current Jira functions for new table ([3b13445](https://github.com/softrams/bulwark/commit/3b134455c991a05a9daa66d970e4824d56aac6be)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **jira update/save:** implement the ability to update/save update ([1d586cf](https://github.com/softrams/bulwark/commit/1d586cf7c0ca12de8a040f4ca65664e8464d3843)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **jira utilities:** save/Update Jira Issue from vulnerabilities ([d2e1c81](https://github.com/softrams/bulwark/commit/d2e1c8135941b84d0e3f700e94bfedcc49da494c)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **jira utility:** implemented JIRA utility POC ([e2aa328](https://github.com/softrams/bulwark/commit/e2aa328d71ca792ad5034cefe433706982b73acd)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **testers column added to assessments summary table:** testers column added to assessment table ([67d5188](https://github.com/softrams/bulwark/commit/67d5188eb7b24b9dec6ef60789b736a15734f8b8)), closes [#205](https://github.com/softrams/bulwark/issues/205) - **vulnerability form:** updated confirmation message for jira export ([8012a9a](https://github.com/softrams/bulwark/commit/8012a9a17630d253c257ce0f0c645fb123580317)), closes [#179](https://github.com/softrams/bulwark/issues/179) ### Bug Fixes - **assessment/vulnerability/report table component:** fixed Jira URL table formatting ([5e053bc](https://github.com/softrams/bulwark/commit/5e053bc73ac3eb3a62e0a9f7e62c1665bc740101)), closes [#225](https://github.com/softrams/bulwark/issues/225) - **dashboard component, organization component, and global styles:** moved card style to global scss ([d9e6154](https://github.com/softrams/bulwark/commit/d9e6154c188091b02a0bf12eeb757dae3fbca0b5)), closes [#165](https://github.com/softrams/bulwark/issues/165) - **jira screenshots and readme:** fixed jira screenshot attachments. Updated README with new gifs ([c13e9b0](https://github.com/softrams/bulwark/commit/c13e9b0f4c2cd723a08f40f47e75330ff5354ff5)) - **update csp:** updated CSP ([6ae8c42](https://github.com/softrams/bulwark/commit/6ae8c4220920f147e0f2149cbe8ecd0b2545db40)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **update undefined message:** updated jiraId URL to use the jira info from database ([6a864d0](https://github.com/softrams/bulwark/commit/6a864d0bf524e5932648436e2d65891ad458cc83)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **user profile component:** fixed user profile form value binding ([1c87039](https://github.com/softrams/bulwark/commit/1c870397518dfe95852a1c9c63911be056c9a45c)), closes [#218](https://github.com/softrams/bulwark/issues/218) ### Tests - **aet controller spec:** add unit tests for new asset controller methods ([0e2213f](https://github.com/softrams/bulwark/commit/0e2213f9e2350289868456b645e294f5a77951aa)), closes [#167](https://github.com/softrams/bulwark/issues/167) - **assessment controller test:** unit test for deleteAssessmentById ([116c0fc](https://github.com/softrams/bulwark/commit/116c0fce41b4b9a3f1cdf11528f5d6e06bfa6350)), closes [#177](https://github.com/softrams/bulwark/issues/177) - **jira utility:** unit tests for the jira utility ([2b0e5d7](https://github.com/softrams/bulwark/commit/2b0e5d7fd46135a6ca33ccaa219ed0e4baf1cd5e)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **tests: asset, assessment, crypto:** added unit tests for the crypto utility and asset controller ([9f63021](https://github.com/softrams/bulwark/commit/9f63021cf2801f99f9b674b9e13efab6636c2f0d)), closes [#179](https://github.com/softrams/bulwark/issues/179) ### Docs - **readme:** updated README ([7ca3343](https://github.com/softrams/bulwark/commit/7ca33432dbd6a3cb8ede463da82b9f600f8666c3)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **readme/contributing:** updated documentation for the README and CONTRIBUTING ([bef6d47](https://github.com/softrams/bulwark/commit/bef6d47bb810c1e58e5579b26bc95f601c203d92)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **security policy:** updated security policy ([c307265](https://github.com/softrams/bulwark/commit/c307265157a3bf2ec03a6eaa9c31280e0dcc4bf8)) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([1e83309](https://github.com/softrams/bulwark/commit/1e8330973b3de1b50cd01a3db102726d46db9b77)) - **deps:** bump @angular-devkit/build-angular in /frontend ([533abf2](https://github.com/softrams/bulwark/commit/533abf27a4f7b86f683254ee618480d5c5064573)) - **deps:** bump @angular-devkit/build-angular in /frontend ([d2a9b04](https://github.com/softrams/bulwark/commit/d2a9b046a7c346ffc35dc1021170f6981dcc1c2b)) - **deps:** bump @angular/cli from 10.0.4 to 10.0.5 in /frontend ([2a7ddc4](https://github.com/softrams/bulwark/commit/2a7ddc4aa2ef15fb41eb55078722a94b1a6d3952)) - **deps:** bump @angular/cli from 10.0.5 to 10.0.6 in /frontend ([1f41210](https://github.com/softrams/bulwark/commit/1f412106b18ed47287a76448ff753e6e7d0d9ffc)) - **deps:** bump @angular/cli from 10.0.6 to 10.0.7 in /frontend ([e093c72](https://github.com/softrams/bulwark/commit/e093c72fd0c8e80aa58b34c91857d6b0aac5bc99)) - **deps:** bump @ng-select/ng-select from 4.0.4 to 5.0.0 in /frontend ([2ea1db8](https://github.com/softrams/bulwark/commit/2ea1db8c23e94bf6b05b79195dc93d26761a4a02)) - **deps:** bump @ng-select/ng-select from 5.0.0 to 5.0.1 in /frontend ([8e04728](https://github.com/softrams/bulwark/commit/8e04728cf09a87878c8b5f2853a0e029176486f6)) - **deps:** bump concurrently from 5.2.0 to 5.3.0 ([55a0601](https://github.com/softrams/bulwark/commit/55a06012daeead903ec88f683ec7a27626f73fd8)) - **deps:** bump helmet from 3.23.3 to 4.0.0 ([2f682de](https://github.com/softrams/bulwark/commit/2f682de0fae94e085c4e92b2cef2a24498b28677)) - **deps:** bump helmet from 4.0.0 to 4.1.0 ([f46bb06](https://github.com/softrams/bulwark/commit/f46bb068c5ad27450584e6573f6f1d6f4e41753c)) - **deps:** bump jira-client from 6.18.0 to 6.19.0 ([369732f](https://github.com/softrams/bulwark/commit/369732fd6c8288db3125597694afdf3c1f14e6df)) - **deps:** bump ngx-markdown from 10.1.0 to 10.1.1 in /frontend ([9a83ce2](https://github.com/softrams/bulwark/commit/9a83ce2e7708a4bc7145aed2a27e5df449b794fd)) - **deps:** bump nodemailer from 6.4.10 to 6.4.11 ([99087f5](https://github.com/softrams/bulwark/commit/99087f5b382499bbbceaeed4b71e42d8e57bf892)) - **deps:** bump password-validator from 5.0.3 to 5.1.0 ([f47a663](https://github.com/softrams/bulwark/commit/f47a663b9bda58a267ddeda3197a100bc4e31f93)) - **deps:** bump rxjs from 6.6.0 to 6.6.2 in /frontend ([2c28258](https://github.com/softrams/bulwark/commit/2c28258b8026eaf59c2a0444bf9d6b62fd693098)) - **deps:** bump ts-node from 8.10.2 to 9.0.0 ([df9971e](https://github.com/softrams/bulwark/commit/df9971e37a7b3c67e35200b2ff91b25f6a4a10c6)) - **deps:** bump tslib from 2.0.0 to 2.0.1 in /frontend ([f320cf9](https://github.com/softrams/bulwark/commit/f320cf9e4819ece06bf3ddf30b62df9eeefbd610)) - **deps:** bump uuid from 8.2.0 to 8.3.0 ([70c2ee1](https://github.com/softrams/bulwark/commit/70c2ee16156d3699c795ba3af91312b33c80de36)) - **deps-dev:** bump @angular/language-service in /frontend ([a5ec3e1](https://github.com/softrams/bulwark/commit/a5ec3e1cb3e22323858c3218d588ed99d6cb514f)) - **deps-dev:** bump @angular/language-service in /frontend ([5ca9ea8](https://github.com/softrams/bulwark/commit/5ca9ea8ed16a653475e2395bd1461792937dfa4d)) - **deps-dev:** bump @angular/language-service in /frontend ([ccebf16](https://github.com/softrams/bulwark/commit/ccebf16a604e94b3a0a1ea4dc7155b35347ac42b)) - **deps-dev:** bump @angular/language-service in /frontend ([9b0fa48](https://github.com/softrams/bulwark/commit/9b0fa48082b5b7285a60388c6d680b3fbfa26dc0)) - **deps-dev:** bump @angular/language-service in /frontend ([8411cd5](https://github.com/softrams/bulwark/commit/8411cd51860464d7be971c16cd6d22a10a73a8aa)) - **deps-dev:** bump @angular/language-service in /frontend ([bf6a329](https://github.com/softrams/bulwark/commit/bf6a3290d3dd785da068ec9500d23b52840c1776)) - **deps-dev:** bump @babel/core from 7.10.5 to 7.11.0 ([296e46a](https://github.com/softrams/bulwark/commit/296e46af651661861900dfd397b09e7ef11fbf40)) - **deps-dev:** bump @babel/core from 7.11.0 to 7.11.1 ([89d900e](https://github.com/softrams/bulwark/commit/89d900eba373dd1b10589d4555f1e810281e9de8)) - **deps-dev:** bump @babel/core from 7.11.1 to 7.11.4 ([daa6fef](https://github.com/softrams/bulwark/commit/daa6fef3d295cea1f72292a0cd29ec3a42404efc)) - **deps-dev:** bump @babel/preset-env from 7.10.4 to 7.11.0 ([598c653](https://github.com/softrams/bulwark/commit/598c653fee8a5d0eac68c924ab4440573d8b2b0c)) - **deps-dev:** bump @commitlint/config-conventional ([2cc4edd](https://github.com/softrams/bulwark/commit/2cc4edd65bf9b519c2a4d20430797ed7b8a21c94)) - **deps-dev:** bump @types/jasmine from 3.5.11 to 3.5.12 in /frontend ([6b0a119](https://github.com/softrams/bulwark/commit/6b0a119264d6fcc9c161a5634a21a6b28902b529)) - **deps-dev:** bump @types/jasmine from 3.5.12 to 3.5.13 in /frontend ([39fd17d](https://github.com/softrams/bulwark/commit/39fd17d21cb2c66f846deacab3cad94428fa8a8e)) - **deps-dev:** bump @types/jest from 26.0.7 to 26.0.8 ([0e8ca5d](https://github.com/softrams/bulwark/commit/0e8ca5dc95c1fab5ff97f8eabe8e08c049e1d9a5)) - **deps-dev:** bump @types/jest from 26.0.8 to 26.0.9 ([3aa5c98](https://github.com/softrams/bulwark/commit/3aa5c98f6e459b79d4d4832696853df4b29b2c75)) - **deps-dev:** bump @types/jest from 26.0.9 to 26.0.10 ([3803bbe](https://github.com/softrams/bulwark/commit/3803bbe0377de9faacb22343e0722772ba01318e)) - **deps-dev:** bump @types/node from 14.0.25 to 14.0.26 in /frontend ([5252e7f](https://github.com/softrams/bulwark/commit/5252e7f80b4c78154bd2850f0b70200d3af259e0)) - **deps-dev:** bump @types/node from 14.0.26 to 14.0.27 in /frontend ([a362060](https://github.com/softrams/bulwark/commit/a36206048d1216aa8b7bb88586eef704dd74af10)) - **deps-dev:** bump @types/node from 14.0.27 to 14.6.0 in /frontend ([a3ba2b6](https://github.com/softrams/bulwark/commit/a3ba2b6c22f059c83490c9c7692836e4c213f5b7)) - **deps-dev:** bump babel-jest from 26.1.0 to 26.2.1 ([a71be6c](https://github.com/softrams/bulwark/commit/a71be6c6e5f8496a95ae9a258733a14dbc6d3c48)) - **deps-dev:** bump babel-jest from 26.2.1 to 26.2.2 ([e376fd1](https://github.com/softrams/bulwark/commit/e376fd1d0353c134000857279232b122b65488a3)) - **deps-dev:** bump babel-jest from 26.2.2 to 26.3.0 ([4e4f147](https://github.com/softrams/bulwark/commit/4e4f147870d4c48a8ae77a71bd70674a41c3dfb8)) - **deps-dev:** bump jest from 26.1.0 to 26.2.2 ([0e2c2a4](https://github.com/softrams/bulwark/commit/0e2c2a4926907bda1041e7f753b4a01e176e72ce)) - **deps-dev:** bump jest from 26.2.2 to 26.3.0 ([a58ad02](https://github.com/softrams/bulwark/commit/a58ad02393078692cf707ed07d4a258a8c6d83fb)) - **deps-dev:** bump jest from 26.3.0 to 26.4.0 ([59f2090](https://github.com/softrams/bulwark/commit/59f2090f62906721d069279fe6e4a34fdeb24f94)) - **deps-dev:** bump jest from 26.4.0 to 26.4.1 ([f50109e](https://github.com/softrams/bulwark/commit/f50109e977dcb06df39c127210bc166b9bd7406b)) - **deps-dev:** bump jest from 26.4.1 to 26.4.2 ([bd3a6f6](https://github.com/softrams/bulwark/commit/bd3a6f65e0f381627d69214ca0dc881f40ecf808)) - **deps-dev:** bump karma from 5.1.0 to 5.1.1 in /frontend ([f96145d](https://github.com/softrams/bulwark/commit/f96145d9dc1ba93892eac309ea903c07df46aaef)) - **deps-dev:** bump karma-jasmine from 3.3.1 to 4.0.0 in /frontend ([1c7a2bd](https://github.com/softrams/bulwark/commit/1c7a2bd3560df20d2ef60ef5f1d34bf45bdb4783)) - **deps-dev:** bump karma-jasmine from 4.0.0 to 4.0.1 in /frontend ([94a8c1c](https://github.com/softrams/bulwark/commit/94a8c1c590dac10f99752e839f7c9bd991328481)) - **deps-dev:** bump standard-version from 8.0.2 to 9.0.0 ([6687d1c](https://github.com/softrams/bulwark/commit/6687d1c537b81af2f8c85e9fb48770137a2938b6)) - **deps-dev:** bump ts-jest from 26.1.3 to 26.1.4 ([cf8231c](https://github.com/softrams/bulwark/commit/cf8231ced3db1c12b5a658993c0abc53d4a7a1b2)) - **deps-dev:** bump ts-jest from 26.1.4 to 26.2.0 ([b0794a1](https://github.com/softrams/bulwark/commit/b0794a1e2774dc4548c4fc7dd78d3c7761ca8f46)) - **deps-dev:** bump tslint from 6.1.2 to 6.1.3 ([010a286](https://github.com/softrams/bulwark/commit/010a286d377393f6157c6db915b5b68c6782517b)) - **deps-dev:** bump tslint from 6.1.2 to 6.1.3 in /frontend ([97e37f1](https://github.com/softrams/bulwark/commit/97e37f1b390d8e75d1ac4367213425676dcfcd1f)) - **documentation updates:** updated documentation and added seed file ([7796d1e](https://github.com/softrams/bulwark/commit/7796d1e2fa75115e523f77c3bf409f68b5b110f7)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **fix merge conflicts:** fixed merge conflicts ([a433792](https://github.com/softrams/bulwark/commit/a43379292ccc42f26af52b5713ac72f88e548ad1)), closes [#167](https://github.com/softrams/bulwark/issues/167) - **fix merge conflicts:** fixed merge conflicts from master ([06da9af](https://github.com/softrams/bulwark/commit/06da9affbf73f3a2c3613ea83955fb73aaa764fb)) - **package.json:** fixing merge conflicts ([4f1c2b3](https://github.com/softrams/bulwark/commit/4f1c2b373d0392ef17b6c07d9d6676b6c3165eff)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **package.jsons:** update jira with develop ([cdb9dfa](https://github.com/softrams/bulwark/commit/cdb9dfac0fc0f12703fbda943a436c21e72e4151)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **remove unneeded files:** removed unused and unneeded interfaces ([d6dbd51](https://github.com/softrams/bulwark/commit/d6dbd51b357a7178097b7a3317f61c4e44657552)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **update dependencies from develop:** updated dependencies from the Develop branch ([94c7bcc](https://github.com/softrams/bulwark/commit/94c7bccf59d52e93369158de5b5e705ad29f4725)), closes [#179](https://github.com/softrams/bulwark/issues/179) - **updated readme:** updated readme and revert workflow name ([614f642](https://github.com/softrams/bulwark/commit/614f64231e45dcf72b8c5a8b81f3d9e52decf142)), closes [#179](https://github.com/softrams/bulwark/issues/179) ### [3.2.3](https://github.com/softrams/bulwark/compare/v3.2.2...v3.2.3) (2020-08-17) ### Bug Fixes - **ormconfig:** updated ormconfig migrations array ([afba021](https://github.com/softrams/bulwark/commit/afba021856cabfde61d7f833998b9be138dcabb2)), closes [#208](https://github.com/softrams/bulwark/issues/208) - **seed initial user fix:** update seed user script so that it uses CLI arguments ([14d0e38](https://github.com/softrams/bulwark/commit/14d0e384228075cb064d794251bcb0d8fff796b1)), closes [#208](https://github.com/softrams/bulwark/issues/208) - **updated readme. added initial migration script:** updated the README with updated initial steps ([c954694](https://github.com/softrams/bulwark/commit/c954694a59e1483c61e7302684d53a57f44e55fa)), closes [#208](https://github.com/softrams/bulwark/issues/208) ### [3.2.2](https://github.com/softrams/bulwark/compare/v3.2.1...v3.2.2) (2020-07-24) ### Others - **deps:** bump @angular-devkit/build-angular in /frontend ([0e7ed13](https://github.com/softrams/bulwark/commit/0e7ed135e28e9529a823a615d1170732bb972ad0)) - **deps:** bump @angular/cli from 10.0.3 to 10.0.4 in /frontend ([9f28641](https://github.com/softrams/bulwark/commit/9f286413a4ea59a5a0d2319a5cfdaf7874f3189b)) - **deps:** bump ngx-markdown from 10.0.0 to 10.1.0 in /frontend ([124ade3](https://github.com/softrams/bulwark/commit/124ade35bcdc635ba8b6a7556e2b91be792c1801)) - **deps:** bump puppeteer from 5.2.0 to 5.2.1 ([21512c8](https://github.com/softrams/bulwark/commit/21512c891e9a719293498f20f0e38314f9bf206f)) - **deps-dev:** bump @angular/language-service in /frontend ([0a5ea43](https://github.com/softrams/bulwark/commit/0a5ea4390fb120edd73a4282431ab4e17153bba7)) - **deps-dev:** bump @types/jest from 26.0.5 to 26.0.7 ([efb36ae](https://github.com/softrams/bulwark/commit/efb36ae6f9d6063f30f1f0a6511cd1f3c5add224)) - **deps-dev:** bump @types/node from 14.0.23 to 14.0.24 in /frontend ([ec93f25](https://github.com/softrams/bulwark/commit/ec93f2539362cc2cf707f3ced68b6a21352bb9ab)) - **deps-dev:** bump @types/node from 14.0.24 to 14.0.25 in /frontend ([49dbb50](https://github.com/softrams/bulwark/commit/49dbb507e7629d7560af0839f2e321083e4c2abf)) - **deps-dev:** bump jasmine-core from 3.5.0 to 3.6.0 in /frontend ([84f70ae](https://github.com/softrams/bulwark/commit/84f70ae75044f8ed1da2635900d47b1521136e95)) ### [3.2.1](https://github.com/softrams/bulwark/compare/v3.2.0...v3.2.1) (2020-07-20) ### Build System - **lru-cache:** added lru-cache to remove angular error ([adaa65b](https://github.com/softrams/bulwark/commit/adaa65ba4c4c1451e0dd7100036e293abb1db824)), closes [#111](https://github.com/softrams/bulwark/issues/111) ### Tests - **assessment controller:** assessment controller unit tests ([c3d7fa8](https://github.com/softrams/bulwark/commit/c3d7fa8f92170657fd4729fa6935af6ed32a6d8c)), closes [#64](https://github.com/softrams/bulwark/issues/64) - **email.service.ts:** unit tests for email service ([799132f](https://github.com/softrams/bulwark/commit/799132fa94466abf9ab137920e5d2382cb6124b9)), closes [#136](https://github.com/softrams/bulwark/issues/136) - **package.json:** update test node script ([6e32fcc](https://github.com/softrams/bulwark/commit/6e32fccea777d9c6aa670a13457767c0ba5169cd)), closes [#137](https://github.com/softrams/bulwark/issues/137) - **password.utility.spec.ts:** unit tests for password utility ([e76fcee](https://github.com/softrams/bulwark/commit/e76fceec53a3d1ee4dd9bb223693c4f1befa7b66)), closes [#64](https://github.com/softrams/bulwark/issues/64) - **seed-user.ts:** initial seed user test ([b8ac8b8](https://github.com/softrams/bulwark/commit/b8ac8b8c2eb34b72efe433111e2291c1dc90ceae)), closes [#137](https://github.com/softrams/bulwark/issues/137) - **user controller:** user controller unit test ([aa53b54](https://github.com/softrams/bulwark/commit/aa53b5469814ed3158cbb3f2436369dadb5a4e07)), closes [#122](https://github.com/softrams/bulwark/issues/122) - **user.controller.spec.ts:** complete unit tests for user controller ([b96af41](https://github.com/softrams/bulwark/commit/b96af417d6b2f49dd95704ead526139c975cffe6)), closes [#122](https://github.com/softrams/bulwark/issues/122) ### Others - **angular 10:** upgrade to Angular 10 ([3fe70f5](https://github.com/softrams/bulwark/commit/3fe70f5498f6367446bed15dedbf92a725de3e00)), closes [#111](https://github.com/softrams/bulwark/issues/111) - **deps:** bump @angular-devkit/build-angular in /frontend ([9aa1a9d](https://github.com/softrams/bulwark/commit/9aa1a9de50bfe870012b3cd0a07870d123f8fc6b)) - **deps:** bump @angular/cli from 10.0.2 to 10.0.3 in /frontend ([cdf5e50](https://github.com/softrams/bulwark/commit/cdf5e50a63d0a97b192afcee6e64098ea6ff3aec)) - **deps:** bump @fortawesome/angular-fontawesome in /frontend ([e7b7aa1](https://github.com/softrams/bulwark/commit/e7b7aa1fc8c0f1e35b27fcad222cabb711ea03b2)) - **deps:** bump @fortawesome/fontawesome-svg-core in /frontend ([a982d4f](https://github.com/softrams/bulwark/commit/a982d4fc8af1d5029a9da26a3b67fc6050e16d6b)) - **deps:** bump @fortawesome/free-brands-svg-icons in /frontend ([7367d70](https://github.com/softrams/bulwark/commit/7367d708a7f08b334e4dedc6a819423e573ddc32)) - **deps:** bump ngx-markdown from 9.1.1 to 10.0.0 in /frontend ([de9eae4](https://github.com/softrams/bulwark/commit/de9eae48f4da2f234d26911c4e118be5930920b4)) - **deps:** bump puppeteer from 5.0.0 to 5.1.0 ([b024375](https://github.com/softrams/bulwark/commit/b0243757789f0341e83eaa1983f782346426f7fd)) - **deps:** bump puppeteer from 5.1.0 to 5.2.0 ([094b493](https://github.com/softrams/bulwark/commit/094b493ec72e1a5003d5691a2f16d69dd69a096f)) - **deps:** bump tslib from 1.10.0 to 1.13.0 in /frontend ([7b031cb](https://github.com/softrams/bulwark/commit/7b031cb6bd5268857f0381b9316c87deb371966a)) - **deps:** bump typescript from 3.9.6 to 3.9.7 in /frontend ([d2fc168](https://github.com/softrams/bulwark/commit/d2fc168e33af7b9c2d24e916c53338a95904093d)) - **deps-dev:** bump @angular/language-service in /frontend ([a1cafaa](https://github.com/softrams/bulwark/commit/a1cafaa370db2ca41f4ae4d05c9a96b23d367f9c)) - **deps-dev:** bump @babel/core from 7.10.4 to 7.10.5 ([cff5ac3](https://github.com/softrams/bulwark/commit/cff5ac3365bb67e4907341d2e1cda3e4a39a5304)) - **deps-dev:** bump @commitlint/cli from 9.0.1 to 9.1.1 ([9670c1a](https://github.com/softrams/bulwark/commit/9670c1a9545c090711639ade18011dae3bfe42b6)) - **deps-dev:** bump @commitlint/config-conventional ([9f987bf](https://github.com/softrams/bulwark/commit/9f987bfd4ee17e9c2dfdd8e2f67bd192374ca634)) - **deps-dev:** bump @types/jasmine from 2.8.16 to 3.5.11 in /frontend ([d529063](https://github.com/softrams/bulwark/commit/d5290638d3ca493428664149591e4829bcff6cef)) - **deps-dev:** bump @types/jest from 26.0.4 to 26.0.5 ([2d442c5](https://github.com/softrams/bulwark/commit/2d442c5629399b815b322ef2c85eb097698ce967)) - **deps-dev:** bump @types/node from 12.12.34 to 14.0.23 in /frontend ([de699d6](https://github.com/softrams/bulwark/commit/de699d6e9ad978ec4613012a7e260977719ccc84)) - **deps-dev:** bump codelyzer from 5.2.2 to 6.0.0 in /frontend ([0cbedf9](https://github.com/softrams/bulwark/commit/0cbedf98485b155c6a459b79a70f9e5ad9817771)) - **deps-dev:** bump karma-coverage-istanbul-reporter in /frontend ([da3906d](https://github.com/softrams/bulwark/commit/da3906d320540c816536c7b77312be6907537992)) - **deps-dev:** bump protractor from 5.4.2 to 7.0.0 in /frontend ([943642e](https://github.com/softrams/bulwark/commit/943642eb31bebe326d6ea55ac757500192a9a704)) - **deps-dev:** bump standard-version from 8.0.0 to 8.0.1 ([c1dc9c2](https://github.com/softrams/bulwark/commit/c1dc9c21d351d7220bba87ef660b2565360edfcc)) - **deps-dev:** bump standard-version from 8.0.0 to 8.0.1 ([5f213bd](https://github.com/softrams/bulwark/commit/5f213bd3532a1857a6425efb48dcf0ad1c8caedf)) - **deps-dev:** bump standard-version from 8.0.1 to 8.0.2 ([7d9979a](https://github.com/softrams/bulwark/commit/7d9979a45300c976d0ecf12d35f5339f6cc111a3)) - **deps-dev:** bump ts-jest from 26.1.1 to 26.1.2 ([13563c8](https://github.com/softrams/bulwark/commit/13563c8036df275f3abd665327df3811c98d14f8)) - **deps-dev:** bump ts-jest from 26.1.2 to 26.1.3 ([aac7b80](https://github.com/softrams/bulwark/commit/aac7b8088eac3cbd1cd1e15c91994b5b1fc1e993)) - **deps-dev:** bump typescript from 3.9.6 to 3.9.7 ([6ba20aa](https://github.com/softrams/bulwark/commit/6ba20aa27d413b209cabd21d5f62111fdc8f1eb2)) - **lodash:** update lodash to mitigate low severity vulnerability ([9fa5682](https://github.com/softrams/bulwark/commit/9fa568233c342224c6042b90b7a5f0db7d454c64)) - **package.json:** move pre-commit hook to hooks ([e2062d6](https://github.com/softrams/bulwark/commit/e2062d639b0a009fca1deb541445f4aada885036)) - **package.json frontend/package.json:** fix merge conflcits ([620e320](https://github.com/softrams/bulwark/commit/620e32043ac680b9b4edc75abd6dec8ba84e881e)), closes [#111](https://github.com/softrams/bulwark/issues/111) - **package.json frontend/package.json:** fix merge conflicts ([244d123](https://github.com/softrams/bulwark/commit/244d1238305041510b570081022d32469ab17877)), closes [#111](https://github.com/softrams/bulwark/issues/111) - **package.json frontend/package.json:** merge from develop. Remove sinon and sqlite3 ([8021985](https://github.com/softrams/bulwark/commit/8021985bfdd45e860a3db54e4643f2d3a92815a7)), closes [#64](https://github.com/softrams/bulwark/issues/64) - **tslint revert:** revert tslint version ([d3f3d97](https://github.com/softrams/bulwark/commit/d3f3d97da8676039262f6355480d79228fe7d0c7)), closes [#111](https://github.com/softrams/bulwark/issues/111) ## [3.2.0](https://github.com/softrams/bulwark/compare/v3.1.3...v3.2.0) (2020-07-13) ### Features - **report.component.html:** implemented dynamic report properties ([8a1300e](https://github.com/softrams/bulwark/commit/8a1300eb51f3ed3971c63cf3e3f0aca9ce37b4d4)), closes [#37](https://github.com/softrams/bulwark/issues/37) ### Bug Fixes - **uuid:** update import statement for uuid ([1e1143f](https://github.com/softrams/bulwark/commit/1e1143f445061f95bab551a55e310fa5862ec737)) ### CI - **node.js.yml:** added linting to CI process ([53f600c](https://github.com/softrams/bulwark/commit/53f600c22ef0127a6702475139e660b20b5b2b2b)), closes [#90](https://github.com/softrams/bulwark/issues/90) - **node.js.yml:** initial commit for github action ([7a87e2b](https://github.com/softrams/bulwark/commit/7a87e2b4ee5dcf7fb09e9d2ca66051a61c4a09a6)), closes [#6](https://github.com/softrams/bulwark/issues/6) - **removing tslint:** removing tslint to fix build error ([02f3c08](https://github.com/softrams/bulwark/commit/02f3c080c4fc487461e27d146e59ed04f00da14c)) ### Others - **deps:** bump @fortawesome/fontawesome-svg-core in /frontend ([4675399](https://github.com/softrams/bulwark/commit/4675399427ab02caab1b79498169fa42bb3e2765)) - **deps:** bump @fortawesome/free-brands-svg-icons in /frontend ([17fb885](https://github.com/softrams/bulwark/commit/17fb885eb8361aedd859c9022084414160826815)) - **deps:** bump @ng-select/ng-select from 4.0.0 to 4.0.4 in /frontend ([be713d0](https://github.com/softrams/bulwark/commit/be713d01a50ca36270948e7e1773968d31579605)) - **deps:** bump @types/body-parser from 1.17.1 to 1.19.0 ([3a4a706](https://github.com/softrams/bulwark/commit/3a4a70613f56896677d490fa504a95cd338b0999)) - **deps:** bump @types/express from 4.17.1 to 4.17.7 ([c41a065](https://github.com/softrams/bulwark/commit/c41a0654ad011afffbac43cf1e3d2a4e4dceab48)) - **deps:** bump bcrypt from 3.0.7 to 5.0.0 ([39f0eeb](https://github.com/softrams/bulwark/commit/39f0eeb7b55d9e98c1252959e3b09cd31869368b)) - **deps:** bump class-validator from 0.10.2 to 0.12.2 ([c5402ca](https://github.com/softrams/bulwark/commit/c5402ca4a77cc09bbd819183606353fec646ed88)) - **deps:** bump concurrently from 5.0.0 to 5.2.0 ([7d63245](https://github.com/softrams/bulwark/commit/7d632452c8146c8e2e97ae6b348378feac9b350a)) - **deps:** bump core-js from 2.6.10 to 3.6.5 in /frontend ([ff968bc](https://github.com/softrams/bulwark/commit/ff968bc39584897fe166869d26f809633cc0b64b)) - **deps:** bump helmet from 3.21.1 to 3.23.3 ([6af23eb](https://github.com/softrams/bulwark/commit/6af23eb373d5fc5b8c6230db6e4f20680fbec932)) - **deps:** bump mysql from 2.17.1 to 2.18.1 ([cf1a858](https://github.com/softrams/bulwark/commit/cf1a858263250f63dbc83fb7323613c5cab42598)) - **deps:** bump ngx-markdown from 8.2.1 to 9.1.1 in /frontend ([2d5574b](https://github.com/softrams/bulwark/commit/2d5574b71f55e5671b96cb78e04f519c253f9ba3)) - **deps:** bump nodemailer from 6.4.2 to 6.4.10 ([9ba41f3](https://github.com/softrams/bulwark/commit/9ba41f31278bd48fe0def48f8d07fc27f30280ec)) - **deps:** bump puppeteer from 1.20.0 to 5.0.0 ([ba72380](https://github.com/softrams/bulwark/commit/ba723801dd54a121a33bdf7dbfba4db4c8245e0a)) - **deps:** bump rxjs from 6.5.4 to 6.6.0 in /frontend ([e28f1ed](https://github.com/softrams/bulwark/commit/e28f1ede03893fe2f12896250c0633c386caeed2)) - **deps:** bump ts-node from 8.4.1 to 8.10.2 ([12dc1fc](https://github.com/softrams/bulwark/commit/12dc1fc8abe24842869f4aa19f574f59be63e921)) - **deps:** bump typeorm from 0.2.24 to 0.2.25 ([dd663d9](https://github.com/softrams/bulwark/commit/dd663d98e0444f40d07e08b7fc92086167f3a3f6)) - **deps:** bump uuid from 3.4.0 to 8.2.0 ([395ae16](https://github.com/softrams/bulwark/commit/395ae16f32cdad745e10ab595e48071b67a3b63f)) - **deps-dev:** bump @angular/language-service in /frontend ([2b48498](https://github.com/softrams/bulwark/commit/2b48498a81444b9ea851e0b1200a998925886b3c)) - **deps-dev:** bump @commitlint/cli from 8.3.5 to 9.0.1 ([cfe0308](https://github.com/softrams/bulwark/commit/cfe030877f9607d896852c905dde2c46b2ad8dc4)) - **deps-dev:** bump @commitlint/config-conventional ([0be3d9b](https://github.com/softrams/bulwark/commit/0be3d9bda8154c9e4a26a332287e6412c4bd983f)) - **deps-dev:** bump karma from 4.0.1 to 5.1.0 in /frontend ([6e63c60](https://github.com/softrams/bulwark/commit/6e63c606da20e29d802bd344dc0110bdd6cbbcac)) - **deps-dev:** bump karma-jasmine from 1.1.2 to 3.3.1 in /frontend ([3a25383](https://github.com/softrams/bulwark/commit/3a2538313299bdb7dfecdd8f0bb087908c47ffcb)) - **deps-dev:** bump nodemon from 1.19.3 to 2.0.4 ([b5ce69b](https://github.com/softrams/bulwark/commit/b5ce69b31822ef59bc9825c8ca4a032385bf182e)) - **deps-dev:** bump ts-node from 7.0.1 to 8.10.2 in /frontend ([2120370](https://github.com/softrams/bulwark/commit/21203709e9bf19d310887c072db185b935c69982)) - **deps-dev:** bump tslint from 5.11.0 to 6.1.2 in /frontend ([14e7ab5](https://github.com/softrams/bulwark/commit/14e7ab56a245c533698d0fb5d17dacddcdb8892f)) - **deps-dev:** bump tslint from 6.1.0 to 6.1.2 ([24b1ed8](https://github.com/softrams/bulwark/commit/24b1ed8db6c8fbf8159b48b2324f583b3ec9e071)) - **deps-dev:** bump typescript from 3.8.3 to 3.9.6 ([b5cd69e](https://github.com/softrams/bulwark/commit/b5cd69ee478ec9d962c68c3eb998636180f60eef)) - **release:** 3.1.4 ([69ce27f](https://github.com/softrams/bulwark/commit/69ce27f54795707ead852816050a4f8c35d3a789)) - **release:** 3.1.5 ([0c3d25a](https://github.com/softrams/bulwark/commit/0c3d25aa2c6a5892b10b43dc985f5b42b4771e6f)) - **release:** 3.1.6 ([db6137b](https://github.com/softrams/bulwark/commit/db6137b8e0a212f3b900dc5760ef2fdcd2e49ac1)) ### [3.1.6](https://github.com/softrams/bulwark/compare/v3.1.5...v3.1.6) (2020-07-11) ### CI - **node.js.yml:** initial commit for github action ([7a87e2b](https://github.com/softrams/bulwark/commit/7a87e2b4ee5dcf7fb09e9d2ca66051a61c4a09a6)), closes [#6](https://github.com/softrams/bulwark/issues/6) ### [3.1.5](https://github.com/softrams/bulwark/compare/v3.1.4...v3.1.5) (2020-07-11) ### Others - **deps:** bump @ng-select/ng-select from 4.0.0 to 4.0.4 in /frontend ([be713d0](https://github.com/softrams/bulwark/commit/be713d01a50ca36270948e7e1773968d31579605)) - **deps:** bump bcrypt from 3.0.7 to 5.0.0 ([39f0eeb](https://github.com/softrams/bulwark/commit/39f0eeb7b55d9e98c1252959e3b09cd31869368b)) - **deps:** bump class-validator from 0.10.2 to 0.12.2 ([c5402ca](https://github.com/softrams/bulwark/commit/c5402ca4a77cc09bbd819183606353fec646ed88)) - **deps:** bump core-js from 2.6.10 to 3.6.5 in /frontend ([ff968bc](https://github.com/softrams/bulwark/commit/ff968bc39584897fe166869d26f809633cc0b64b)) - **deps:** bump ngx-markdown from 8.2.1 to 9.1.1 in /frontend ([2d5574b](https://github.com/softrams/bulwark/commit/2d5574b71f55e5671b96cb78e04f519c253f9ba3)) - **deps:** bump puppeteer from 1.20.0 to 5.0.0 ([ba72380](https://github.com/softrams/bulwark/commit/ba723801dd54a121a33bdf7dbfba4db4c8245e0a)) - **deps:** bump rxjs from 6.5.4 to 6.6.0 in /frontend ([e28f1ed](https://github.com/softrams/bulwark/commit/e28f1ede03893fe2f12896250c0633c386caeed2)) - **deps:** bump ts-node from 8.4.1 to 8.10.2 ([12dc1fc](https://github.com/softrams/bulwark/commit/12dc1fc8abe24842869f4aa19f574f59be63e921)) - **deps:** bump typeorm from 0.2.24 to 0.2.25 ([dd663d9](https://github.com/softrams/bulwark/commit/dd663d98e0444f40d07e08b7fc92086167f3a3f6)) - **deps-dev:** bump @commitlint/cli from 8.3.5 to 9.0.1 ([cfe0308](https://github.com/softrams/bulwark/commit/cfe030877f9607d896852c905dde2c46b2ad8dc4)) - **deps-dev:** bump @commitlint/config-conventional ([0be3d9b](https://github.com/softrams/bulwark/commit/0be3d9bda8154c9e4a26a332287e6412c4bd983f)) - **deps-dev:** bump karma from 4.0.1 to 5.1.0 in /frontend ([6e63c60](https://github.com/softrams/bulwark/commit/6e63c606da20e29d802bd344dc0110bdd6cbbcac)) - **deps-dev:** bump karma-jasmine from 1.1.2 to 3.3.1 in /frontend ([3a25383](https://github.com/softrams/bulwark/commit/3a2538313299bdb7dfecdd8f0bb087908c47ffcb)) - **deps-dev:** bump nodemon from 1.19.3 to 2.0.4 ([b5ce69b](https://github.com/softrams/bulwark/commit/b5ce69b31822ef59bc9825c8ca4a032385bf182e)) - **deps-dev:** bump tslint from 5.11.0 to 6.1.2 in /frontend ([14e7ab5](https://github.com/softrams/bulwark/commit/14e7ab56a245c533698d0fb5d17dacddcdb8892f)) - **deps-dev:** bump tslint from 6.1.0 to 6.1.2 ([24b1ed8](https://github.com/softrams/bulwark/commit/24b1ed8db6c8fbf8159b48b2324f583b3ec9e071)) ### [3.1.4](https://github.com/softrams/bulwark/compare/v3.1.3...v3.1.4) (2020-07-11) ### [3.1.3](https://github.com/softrams/bulwark/compare/v3.1.2...v3.1.3) (2020-07-10) ### Others - **fix dependabot config:** fix dependabot config errors ([f4acc17](https://github.com/softrams/bulwark/commit/f4acc17bb797a16214f761239391477795d5ba9d)) ### [3.1.2](https://github.com/softrams/bulwark/compare/v3.1.1...v3.1.2) (2020-07-10) ### Others - **add target branch:** add target branch to dependabot ([8a5732b](https://github.com/softrams/bulwark/commit/8a5732bceb32b6ee44f4b83837112090643b5a59)) ### [3.1.1](https://github.com/softrams/bulwark/compare/v3.1.0...v3.1.1) (2020-07-10) ### Others - **add dependabot config:** add dependabot configuration ([bcf46b1](https://github.com/softrams/bulwark/commit/bcf46b146b2240c4d7269caf3df2ee413771d431)), closes [#65](https://github.com/softrams/bulwark/issues/65) ## [3.1.0](https://github.com/softrams/bulwark/compare/v3.0.2...v3.1.0) (2020-07-09) ### Features - **user-profile component:** ability to update user password ([9855987](https://github.com/softrams/bulwark/commit/985598765c7e7a4002bc5dd679a2096a1833dbc6)), closes [#50](https://github.com/softrams/bulwark/issues/50) - **user-profile.component.ts:** wired API to auth service and user prof ([6b6eb36](https://github.com/softrams/bulwark/commit/6b6eb36b1a361d2c3601e6cb7d4bac24a80b5c0c)), closes [#50](https://github.com/softrams/bulwark/issues/50) ### Bug Fixes - **babel.config.js:** added new line ([e0617dd](https://github.com/softrams/bulwark/commit/e0617dd8e0d7c5c3b71ec88cc903e22632f5c418)) - **jest.config.js:** removed tsx and jsx ([a269233](https://github.com/softrams/bulwark/commit/a26923324c0fbff7a32617fb5cc9298ef53df816)) - **jwt.spec.ts:** added comments ([3b1505b](https://github.com/softrams/bulwark/commit/3b1505bbe7a449a32188f97f1fdbee4f6f1de63e)) - **jwt.spec.ts:** added process.env to beforeAll to fix the issue ([51ab6e8](https://github.com/softrams/bulwark/commit/51ab6e805df007f3dbc41c9864670b2716ed9afd)) - **jwt.spec.ts:** readded dotenv.config per PR ([e6ff159](https://github.com/softrams/bulwark/commit/e6ff1590f177c0460976360682e84935a8cc848b)) - **jwt.spec.ts:** removed need for process.env ([c8ab992](https://github.com/softrams/bulwark/commit/c8ab99274859d9287ff21f9e82416dd2ef1d73db)) - **package.json:** added needed packages to package.json ([15708b7](https://github.com/softrams/bulwark/commit/15708b7a19eed9a65be054876e4b231d2cee02ca)) - **testing.md:** fixed a typo issue ([5441e7b](https://github.com/softrams/bulwark/commit/5441e7b43bba629bff0eaef374355e2f45163938)) - **tsconfig.json:** added spec.ts to the exclude list ([66321e1](https://github.com/softrams/bulwark/commit/66321e1284adc186fdc645a4cdc65f6b74b992f5)) ### Code Refactoring - **user-profile.component.html .spec.ts:** updated element ID ([e91f40c](https://github.com/softrams/bulwark/commit/e91f40c820bd340f0b8f8f1472be7ecf737ba342)), closes [#50](https://github.com/softrams/bulwark/issues/50) ### Docs - **contributing.md:** added a testing section under the linting section ([332e174](https://github.com/softrams/bulwark/commit/332e174f34a3b1bb996c7af567478b9a33dcfe9f)) - **contributing.md:** removed testing information and added testing.md ([f3bdc44](https://github.com/softrams/bulwark/commit/f3bdc443cea498a8ce1fcf6f6687475b2c4c3c61)) - **testing.md:** added a line to open the test coverage ([95d4862](https://github.com/softrams/bulwark/commit/95d48623d0a929bc667b92e0ee6b93484bdb1b54)) - **testing.md:** removed comment of early stages ([f4ccdb9](https://github.com/softrams/bulwark/commit/f4ccdb92c1ac9dfe30f9a8241f15f7162c058a3e)) - **testings.md:** added testing doc to run down requirements ([4dc0672](https://github.com/softrams/bulwark/commit/4dc0672e7cd267201ff3126a9363b8758e3fe037)) ### [3.0.2](https://github.com/softrams/bulwark/compare/v3.0.1...v3.0.2) (2020-05-28) ### Docs - **seed-user.ts:** updated documentation ([1797f44](https://github.com/softrams/bulwark/commit/1797f4412cd1cfca0c8fa3ce54709a9aa81b8d34)) ### [3.0.1](https://github.com/softrams/bulwark/compare/v3.0.0...v3.0.1) (2020-05-28) ### Bug Fixes - **seed-user.ts:** update seed user with new fields ([a7881b8](https://github.com/softrams/bulwark/commit/a7881b80d2baf4a017d1cb70661002dce65f20b8)), closes [#57](https://github.com/softrams/bulwark/issues/57) ## [3.0.0](https://github.com/softrams/bulwark/compare/v2.0.1...v3.0.0) (2020-05-28) ### ⚠ BREAKING CHANGES - **assessment form and report:** ManyToMany relationship has been created between the User and Assessment models. API's have been updated for this change. New API's created to retrieve users. ### Features - **assessment form and report:** dynamic tester association to asssment ([3bbfc9c](https://github.com/softrams/bulwark/commit/3bbfc9c4d3d3a8f801ef3691298de223cea10dcc)), closes [#52](https://github.com/softrams/bulwark/issues/52) ### Bug Fixes - **angular datepipe:** added UTC property to datepipe ([701611f](https://github.com/softrams/bulwark/commit/701611f94970502b327bd29f22cd99c090892359)), closes [#3](https://github.com/softrams/bulwark/issues/3) ### Code Refactoring - **assessment controller:** updated response message. Removed usrId ([c2d2555](https://github.com/softrams/bulwark/commit/c2d2555e48aa86be0e13f1778297d1cdb73efb75)), closes [#52](https://github.com/softrams/bulwark/issues/52) ### [2.0.1](https://github.com/softrams/bulwark/compare/v2.0.0...v2.0.1) (2020-05-20) ### Docs - **readme.md:** added new env var and fixed typo ([5ccf6ac](https://github.com/softrams/bulwark/commit/5ccf6ac022ee1012141b2518848443de04b5f19c)) ## 2.0.0 (2020-05-20) ### ⚠ BREAKING CHANGES - **register component and api:** The register API was updated to include the missing fields. User validation was also added to the Register API. - Update to the login API and jwt middleware - **patch and retrieve user:** Added three new columns for the User table: firstName, lastName, title ### Features - **app interceptor:** original request now is sent after refresh ([5072fce](https://github.com/softrams/bulwark/commit/5072fce3a11dd8a8e51cfa46f6adc4d51ed843f3)), closes [#5](https://github.com/softrams/bulwark/issues/5) - implemented Refresh Token ([21522ac](https://github.com/softrams/bulwark/commit/21522ac7373b310629d80b584687dac3d09a646e)), closes [#5](https://github.com/softrams/bulwark/issues/5) - **patch and retrieve user:** implemented APIs for user patch and get ([fefd70b](https://github.com/softrams/bulwark/commit/fefd70b64c4fe38136ea3b741cb435aa637c118c)), closes [#4](https://github.com/softrams/bulwark/issues/4) - **user profile component:** created user profile template ([d2431af](https://github.com/softrams/bulwark/commit/d2431afcbd67f5adc7612ee100997d4d79a48fce)), closes [#4](https://github.com/softrams/bulwark/issues/4) ### Bug Fixes - **register component and api:** added missing user fields to registeAPI ([6f73ef3](https://github.com/softrams/bulwark/commit/6f73ef34fa8f34f787dfa265a293301d12673dae)) ### Docs - **contributing.md:** modified pull request process ([8f4adce](https://github.com/softrams/bulwark/commit/8f4adce1993d873e77593b9063997a5134be3d59)) ### Tests - **user-profile component:** added unit tests to the user-profile comp ([91ee0c7](https://github.com/softrams/bulwark/commit/91ee0c7173e48af10ba8451000b8b0b456620b4e)), closes [#4](https://github.com/softrams/bulwark/issues/4) ### Code Refactoring - **login.component:** remove console.log. Used auth service func ([30eae04](https://github.com/softrams/bulwark/commit/30eae049a9b912de729e13b8a5a93c67baa7bfc3)), closes [#5](https://github.com/softrams/bulwark/issues/5) ### Others - **angular.json:** turned off google analytics ([1a849c5](https://github.com/softrams/bulwark/commit/1a849c5b0127f01805eb7444aa3668f171281d53)) - **implemented commitizen and husky:** implented commit mgs standards ([16c3e43](https://github.com/softrams/bulwark/commit/16c3e432e20ad7f7c94a5a857631a9d4b12c8cdc)) - **implemented husky, commitizen, and pull request template:** commit ([f14b5b2](https://github.com/softrams/bulwark/commit/f14b5b25ef2dcdc3d07c5fb899ea883cc9b51f65)), closes [#43](https://github.com/softrams/bulwark/issues/43) - **package.json:** installed standard version ([4390b61](https://github.com/softrams/bulwark/commit/4390b616c3176a77acd98ef2ad9d3d76c900a034)), closes [#42](https://github.com/softrams/bulwark/issues/42) - **package.json:** updated contributors and set version to 1 ([0910ebe](https://github.com/softrams/bulwark/commit/0910ebe1bd57bf5f3c5cb4b8b3cd066c7d864c3c)) ================================================ FILE: CODEOWNERS ================================================ * @softrams/bulwark-core-team ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@softrams.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing When contributing to this repository, please first discuss the change you wish to make via issue with the owners of this repository before making a change. ## Contributing to Development Issues will be labelled with `help wanted` or `good first issue` - `Help wanted` label indicates tasks where the project team would appreciate community help - `Good first issue` label indicates a task that introduce developers to the project, have low complexity, and are isolated ## Version Control The project uses git as its version control system and GitHub as the central server and collaboration platform. ### Branching model Bulwark is maintained in a simplified [Gitflow](https://jeffkreeftmeijer.com/git-flow/) fashion, where all active development happens on the develop branch while master is used to maintain stable versions. Tasks with higher complexity, prototypes, and experiments will occur in feature branches. ### Versioning Any release from master will have a unique version automated by [standard-version](https://github.com/conventional-changelog/standard-version) `MAJOR.MINOR.PATCH` will be incremented by: 1. `MAJOR` version when breaking changes occur 2. `MINOR` version with new functionality that is backwards-compatible 3. `PATCH` version with backwards-compatible bug fixes ## Pull-Request Process 1. All work must be done in a fork off the dev branch 2. Ensure any install or build dependencies are removed 3. All Git commits within a PR must be conventional commits using [commitizen](https://github.com/commitizen/cz-cli) and enforced by [husky](https://github.com/typicode/husky) 1. Run `$ npm run commit` when committing changes 4. The code must comply to the [testing requirements](TESTING.md) 5. Open a Pull-Request against the `develop` branch ================================================ FILE: Dockerfile ================================================ FROM softramsdocker/bulwark-base:latest USER root # Environment Arguments for Bulwark ARG MYSQL_USER ARG MYSQL_ROOT_PASSWORD ARG MYSQL_DB_CHECK ARG DB_PASSWORD ARG DB_URL ARG DB_USERNAME ARG DB_PORT ARG DB_NAME ARG DB_TYPE ARG NODE_ENV ARG DEV_URL ARG SERVER_ADDRESS ARG PORT ARG JWT_KEY ARG JWT_REFRESH_KEY ARG CRYPTO_SECRET ARG CRYPTO_SALT # Stage the setup to launch Bulwark RUN mkdir -p /bulwark COPY . /bulwark WORKDIR "bulwark" # Permissions for Bulwark RUN chown -R bulwark:bulwark /bulwark # DB Wait MySQL Status Up, requires mysql-client and python RUN apk add --no-cache --update mysql-client \ python2 # Runas User USER bulwark # Bulwark Specific Startup # Cleanup NPM to save some space RUN npm install \ && rm -rf /bulwark/.npm # Swap to root and delete python USER root # Clean up apk RUN apk del python2 # Runas User USER bulwark # Running Port EXPOSE 5000 # Launch Bulwark CMD ["npm", "run", "start"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Softrams LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

An organizational asset and vulnerability management tool, with Jira integration, designed for generating application security reports.

## Features - Multi-client Vulnerability Management - Security Report Generation - Jira Integration - Team-based Roles Authorization - API Key & Management - Email Integration - Markdown Support - User Activation/Deactivation (Admin) ## Note Please keep in mind, this project is in early development. ## Demo ![Bulwark Walkthrough Demo](https://github.com/softrams/media/blob/main/bulwark_report_demo.gif) ## Jira Integration ![Bulwark Jira Demo](https://github.com/softrams/media/blob/main/bulwark_jira_demo.gif) ## Launch with Docker 1. Install [Docker](https://www.docker.com/) 2. Create a `.env` file and supply the following properties: ``` MYSQL_DATABASE="bulwark" MYSQL_PASSWORD="bulwark" MYSQL_ROOT_PASSWORD="bulwark" MYSQL_USER="root" MYSQL_DB_CHECK="mysql" DB_PASSWORD="bulwark" DB_URL="172.16.16.3" DB_ROOT="root" DB_USERNAME="bulwark" DB_PORT=3306 DB_NAME="bulwark" DB_TYPE="mysql" NODE_ENV="production" DEV_URL="http://localhost:4200" SERVER_ADDRESS="http://localhost" PORT=4500 JWT_KEY="changeme" JWT_REFRESH_KEY="changeme" CRYPTO_SECRET="changeme" CRYPTO_SALT="changeme" ``` Build and start Bulwark containers: ``` docker-compose up ``` Start/Stop Bulwark containers: ``` docker-compose start docker-compose stop ``` Remove Bulwark containers: ``` docker-compose down ``` Bulwark will be available at [localhost:4500](http://localhost:4500) ## Local Installation ``` $ git clone (url) $ cd bulwark $ npm install ``` Running `npm install` will install both server-side and client-side modules. Furthermore, it will run the script `npm run config` which will dynamically set the environment variables in addition to updating the [Angular environment](https://angular.io/guide/build). ### Development Mode Set `NODE_ENV="development"` ``` $ npm run config $ npm run start:dev ``` ### Production Mode Set `NODE_ENV="production"` _Please note: `npm install` will automatically build in production mode_ ``` $ npm run config $ npm run build:prod $ npm start ``` ### Environment variables Create a `.env` file on the root directory. This will be parsed with [dotenv](https://www.npmjs.com/package/dotenv) by the application. #### `DB_PASSWORD` `DB_PASSWORD="somePassword"` Set this variable to database password #### `DB_USERNAME` `DB_USERNAME="foobar"` Set this variable to database user name #### `DB_URL` `DB_URL=something-foo-bar.dbnet` Set this variable to database URL #### `DB_PORT` `DB_PORT=3306` Set this variable to database port #### `DB_NAME` `DB_NAME="foobar"` Set this variable to database connection name #### `DB_TYPE` `DB_TYPE="mysql"` The application was developed using a MySQL database. See the [typeorm](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#common-connection-options) documentation for more database options. #### `NODE_ENV` `NODE_ENV=production` Set this variable to determine node environment #### `DEV_URL="http://localhost:4200"` Used by Angular to build and serve the application #### `SERVER_ADDRESS="http://localhost"` Update if a different server address is required #### `PORT=4500` Update if a different server port is required #### `JWT_KEY` `JWT_KEY="changeMe"` Set this variable to the JWT secret #### `JWT_REFRESH_KEY` `JWT_REFRESH_KEY="changeMe"` Set this variable to the refresh JWT secret #### `CRYPTO_SECRET` `CRYPTO_SECRET="randomValue"` Set this variable to the [Scrypt](https://nodejs.org/api/crypto.html#crypto_crypto_scryptsync_password_salt_keylen_options) password. #### `CRYPTO_SALT` `CRYPTO_SECRET="randomValue"` Set this variable to the [Scrypt](https://nodejs.org/api/crypto.html#crypto_crypto_scryptsync_password_salt_keylen_options) salt. ### Empty `.env` file template ``` DB_PASSWORD="" DB_URL="" DB_USERNAME="" DB_PORT=3306 DB_NAME="" DB_TYPE="" NODE_ENV="" DEV_URL="http://localhost:4200" SERVER_ADDRESS="http://localhost" PORT=4500 JWT_KEY="" JWT_REFRESH_KEY="" CRYPTO_SECRET="" CRYPTO_SALT="" ``` ### Note on M1/M2 Macs ``` Install sqlite3: brew install sqlite3 Export compiler related env variables: export LDFLAGS="-L/opt/homebrew/opt/sqlite/lib" export CPPFLAGS="-I/opt/homebrew/opt/sqlite/include" export PKG_CONFIG_PATH="/opt/homebrew/opt/sqlite/lib/pkgconfig" export NODE_OPTIONS=--openssl-legacy-provider Prepare for a fresh install: rm -rf node_modules npm cache verify npm i --force ``` ### Create Initial Database Migration 1. Create the initial database migration ``` $ npm run migration:init ``` 2. Run the initial database migration ``` $ npm run migration:run ``` ## Default credentials A user account is created on initial startup with the following credentials: - email: `admin@example.com` - password: `changeMe` Upon first login, update the default user password under the profile section. ## Roles The application utilizes least privilege access with team-based authorization. Teams are assigned a role which determines the features available to that specific team. A user will inherit roles from team membership. Administrators have team management access and must assign users to teams. Initially, users are created with no team association and will not have access to any features in the application. The three roles include: 1. Admin 2. Tester 3. Read-Only A team can only be associated to a single organization. However, a team can be associated to multiple assets within the same organization. A user can be a member of multiple teams. If a user is assigned to multiple teams of the same organization, the system will choose the highest authorized team. _Please note: The default user is automatically assigned to the `Administrators` team on initial startup_ ### Role Matrix
Admin Tester Read-Only
User-Profile Management x x x
Team Management x
User Management x
Invite User x
Create User x
Email Settings Management x
Jira Integration x
Organization: Read x x x
Organization: Full Write x
Asset: Read x x x
Asset: Full Write x
Assessment: Read x x x
Assessment: Full Write x x
Vulnerability: Read x x x
Vulnerability: Full Write x x
Export Vulnerability to Jira x x
Report Generation x x x

## API Key & Management A user may generate a single API key which can be used in place of their authorization token. This API key allows for all actions against the application that the user is authorized for. ### Generating an API key pair 1. Login to the application 2. Navigate to the `User Profile` section 3. Select `Generate API Key` This action will generate a pair of keys: 1. `Bulwark-Api-Key` 1. This is a generated plaintext value to identify the user. 2. `Bulwark-Secret-Key` 1. This is a generated plaintext value to verify the user by comparing a [Bcrypt](https://www.npmjs.com/package/bcrypt) hash stored in the database. Write down the generated keys in a safe place. You will not be able to retrieve the keys at a later time. ### How to use API keys The API key pair values must be matched and appended to the following HTTP request headers: - `Bulwark-Api-Key` - `Bulwark-Secret-Key` Example: ``` GET /api/assessment/1 HTTP/1.1 Host: localhost:4500 Accept: application/json, text/plain, */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Bulwark-Api-Key: {{changeMe}} Bulwark-Secret-Key: {{changeMe}} Origin: http://localhost:4200 Connection: close Referer: http://localhost:4200/ Pragma: no-cache Cache-Control: no-cache ``` ## Built With - [Typeorm](https://typeorm.io/#/) - The ORM used - [Angular](https://angular.io/) - The Angular Framework - [Express](https://expressjs.com/) - A minimal and flexible Node.js web application framework ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Read the [contribution guidelines](CONTRIBUTING.md) for more information. ## License [MIT](https://choosealicense.com/licenses/mit/) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Security patches will be included in the latest released version. ## Reporting a Vulnerability For any vulnerabilities found within Bulwark, please contact oss-security-reports@softrams.com ### Description Describe the nature of the vulnerability. ### Problem Locations Describe where the in the application the vulnerability occurs. If possible, add the affected code. For ex: | Problem Location | Target | | --------------------------- | ------------------ | | foo/bar/admin.js | Line 101, 106, 200 | | localhost:5000/#/admin/{id} | id | ### Remediation (optional) Describe any suggestions on how to fix the vulnerability. ================================================ FILE: TESTING.md ================================================ # Testing Requirements 1. All new and updated **back-end** code should have a corresponding unit tests 2. All new and existing unit tests should pass locally before opening a Pull-Request 3. Linting should pass locally before opening a Pull-Request ## Running Front-End Unit Tests (Not Currently Required) - The Angular unit tests have **not** been implemented so please skip this section - Run `npm run test:front` to execute the unit tests via [Karma](https://karma-runner.github.io) - Follow the recommended guidelines for Front-End [Testing](https://angular.io/guide/testing) ## Running Back-End Unit Tests - Run `npm run test:node` to execute the unit tests via [Jest](https://jestjs.io/) - Test coverage report can be opened in browser located in: `coverage/lcov-report/index.html` - Follow the recommended guidelines for Back-End [Testing](https://jestjs.io/) ## Running All Tests - Run `npm run test` to execute the unit tests. ## Linting ``` npm run lint ``` In case your PR is failing from style guide issues try running `npm run lint:fix` - this will fix all syntax or code style issues automatically without breaking your code. ### Prettier Bulwark uses [Prettier](https://prettier.io/) for opinionating code formatting. It is recommended to run it from your editor. Use the following [steps](https://prettier.io/docs/en/editors.html) to integrate Prettier into your editor. ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ ['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript', ], }; ================================================ FILE: buildspec.yaml ================================================ version: 0.2 phases: pre_build: on-failure: ABORT commands: - echo Logging in to Docker Hub... - docker login --username ${DOCKERHUB_USER} --password ${DOCKERHUB_TOKEN} - | if [ -z $CODEBUILD_WEBHOOK_TRIGGER ] then TAG="latest" else if [ "${CODEBUILD_WEBHOOK_TRIGGER}" = "branch/master" ] then TAG="latest" else echo "in else ${CODEBUILD_WEBHOOK_TRIGGER}" TAG=${CODEBUILD_WEBHOOK_TRIGGER/branch\//} fi fi - echo $TAG build: on-failure: ABORT commands: - echo Build started on `date` - echo Building the Docker image... - echo "docker build -t softramsdocker/bulwark:${TAG} ." - docker build -t softramsdocker/bulwark:${TAG} . post_build: commands: - echo Build completed on `date` - echo Pushing the Docker image... - docker push softramsdocker/bulwark:${TAG} ================================================ FILE: bulwark_base/Dockerfile ================================================ # Softrams - Bulwark Reporting Application Dockerized Configuration # Maintained by Bill Jones # Start from Alpine Linux for smaller footprint FROM alpine:latest # Environmental Items ENV NODE_VERSION=20.14.0 \ TYPESCRIPT_VERSION=10.9.2 \ PUPPETEER_VERSION=23.2.0 \ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser # View template at https://github.com/nodejs/docker-node/blob/master/Dockerfile-alpine.template # Setup Bulwark Container User RUN addgroup -S bulwark && adduser -S bulwark -G bulwark -s /bin/sh -D bulwark # Update Image RUN apk upgrade --no-cache -U # Install Required Packages to Build NodeJS and Puppeter Items RUN apk add --no-cache --virtual .build-deps-full curl make gcc g++ python3 linux-headers binutils-gold gnupg libstdc++ chromium \ fontconfig udev ttf-freefont fontconfig pango-dev libxcursor libxdamage cups-libs dbus-libs libxrandr \ libxscrnsaver libc6-compat nss freetype freetype-dev harfbuzz ca-certificates libgcc py-setuptools # Ingest the GPG Keys from https://github.com/nodejs/node#release-keys RUN for server in keys.openpgp.org pool.sks-keyservers.net keyserver.pgp.com ha.pool.sks-keyservers.net; do \ gpg --keyserver $server --recv-keys \ 4ED778F539E3634C779C87C6D7062848A1AB005C \ 141F07595B7B3FFE74309A937405533BE57C7D57 \ 74F12602B6F1C4E913FAA37AD3A89613643B6201 \ DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7 \ CC68F5A3106FF448322E48ED27F5E38D5B0A215F \ 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \ 890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 \ C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C \ 108F52B48DB57BB0CC439B2997B01419BD92F80A \ A363A499291CBBC940DD62E41F10027AF002F8B0 && break; \ done # Perform nodejs installation # https://nodejs.org/dist/v14.9.0/node-v14.9.0.tar.gz - URL for Nodejs Source Code RUN curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.gz" \ && curl -fsSLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" \ && curl -fsSLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.sig" \ #&& gpg --verify SHASUMS256.txt.sig SHASUMS256.txt \ && grep " node-v$NODE_VERSION.tar.gz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xf "node-v$NODE_VERSION.tar.gz" \ && cd "node-v$NODE_VERSION" \ && ./configure \ && make -j$(getconf _NPROCESSORS_ONLN) V= \ && make install \ && apk del .build-deps-full \ && cd .. \ && rm -Rf "node-v$NODE_VERSION" \ && rm "node-v$NODE_VERSION.tar.gz" SHASUMS256.txt.sig SHASUMS256.txt \ # Cleanup RUN rm -f "node-v$NODE_VERSION" \ # smoke tests && node --version \ && npm --version \ # Setup for launch control of Bulwark WORKDIR / COPY bulwark-entrypoint /usr/local/bin/ ENTRYPOINT ["bulwark-entrypoint"] ================================================ FILE: bulwark_base/buildspec.yaml ================================================ version: 0.2 phases: pre_build: commands: - echo Logging in to Docker Hub... - docker login --username ${DOCKERHUB_USER} --password ${DOCKERHUB_TOKEN} build: on-failure: ABORT commands: - echo Build started on `date` - echo Building the Docker image... - cd bulwark_base - docker build -t softramsdocker/bulwark-base:latest . post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images... - docker push softramsdocker/bulwark-base:latest ================================================ FILE: bulwark_base/bulwark-entrypoint ================================================ #!/bin/sh exec "$@" ================================================ FILE: commitlint.config.js ================================================ module.exports = { extends: ['@commitlint/config-conventional'] }; ================================================ FILE: docker-compose.yml ================================================ version: '3.8' services: bulwark: image: softramsdocker/bulwark:latest container_name: bulwark environment: MYSQL_USER: '${MYSQL_USER}' MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASSWORD}' MYSQL_DB_CHECK: '${MYSQL_DB_CHECK}' DB_PASSWORD: '${DB_PASSWORD}' DB_URL: '${DB_URL}' DB_USERNAME: '${DB_USERNAME}' DB_PORT: '${DB_PORT}' DB_NAME: '${DB_NAME}' DB_TYPE: '${DB_TYPE}' NODE_ENV: '${NODE_ENV}' DEV_URL: '${DEV_URL}' SERVER_ADDRESS: '${SERVER_ADDRESS}' PORT: '${PORT}' JWT_KEY: '${JWT_KEY}' JWT_REFRESH_KEY: '${JWT_REFRESH_KEY}' CRYPTO_SECRET: '${CRYPTO_SECRET}' CRYPTO_SALT: '${CRYPTO_SALT}' depends_on: - bulwark-db networks: static-network: ipv4_address: 172.16.16.2 ports: - '5000:5000' expose: - '5000' stop_grace_period: 1m volumes: - bulwark-temp:/bulwark/src/temp:rw command: > sh -c " until mysql --host=$${DB_URL} --user=$${MYSQL_USER} --password=$${MYSQL_ROOT_PASSWORD} --database=$${MYSQL_DB_CHECK} -e 'SELECT user FROM user;'; do >&2 echo MySQL is unavailable - sleeping sleep 1 done && echo MySQL should be up - starting up Bulwark. && npm run postinstall && echo Initial DB Creation && npm run docker:check && npm run start" bulwark-db: image: mysql:9.0.1 # 7.7.31 container_name: bulwark_db environment: MYSQL_DATABASE: '${DB_NAME}' MYSQL_USER: '${DB_USERNAME}' MYSQL_PASSWORD: '${DB_PASSWORD}' MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' networks: static-network: ipv4_address: 172.16.16.3 ports: - '3306:3306' expose: - '3306' volumes: - bulwark-db:/var/lib/mysql:rw restart: always volumes: bulwark-db: bulwark-temp: networks: static-network: ipam: config: - subnet: 172.16.16.0/29 ================================================ FILE: frontend/.browserslistrc ================================================ # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed > 0.5% last 2 versions Firefox ESR not dead not IE 9-11 ================================================ FILE: frontend/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: frontend/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist /tmp /out-tsc # Only exists if Bazel was run /bazel-out /src/environments # dependencies /node_modules # profiling files chrome-profiler-events.json speed-measure-plugin.json # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # misc /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log yarn-error.log testem.log /typings # System Files .DS_Store Thumbs.db ================================================ FILE: frontend/README.md ================================================ # Frontend This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. ## Running unit tests 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 [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 README](https://github.com/angular/angular-cli/blob/master/README.md). ================================================ FILE: frontend/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "frontend": { "root": "", "sourceRoot": "src", "projectType": "application", "prefix": "app", "schematics": { "@schematics/angular:component": { "style": "sass" } }, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/frontend", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": [ "src/styles.scss", "./node_modules/prismjs/themes/prism-okaidia.css", "./node_modules/primeng/resources/themes/saga-blue/theme.css", "./node_modules/primeng/resources/primeng.min.css", "./node_modules/primeicons/primeicons.css", "./node_modules/primeflex/primeflex.css" ], "scripts": [ "./node_modules/marked/bin/marked.js", "./node_modules/prismjs/prism.js" ], "vendorChunk": true, "extractLicenses": false, "buildOptimizer": false, "sourceMap": true, "optimization": false, "namedChunks": true }, "configurations": { "development": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.dev.ts" } ] }, "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "5mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb" } ] } } }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "frontend:build" }, "configurations": { "production": { "browserTarget": "frontend:build:production" }, "development": { "browserTarget": "frontend:build:development" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "frontend:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.spec.json", "karmaConfig": "src/karma.conf.js", "styles": ["src/styles.scss"], "scripts": [], "assets": ["src/favicon.ico", "src/assets"] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], "exclude": ["**/node_modules/**"] } } } }, "frontend-e2e": { "root": "e2e/", "projectType": "application", "prefix": "", "architect": { "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { "protractorConfig": "e2e/protractor.conf.js", "devServerTarget": "frontend:serve" }, "configurations": { "production": { "devServerTarget": "frontend:serve:production" } } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": "e2e/tsconfig.e2e.json", "exclude": ["**/node_modules/**"] } } } } }, "defaultProject": "frontend", "cli": { "analytics": false } } ================================================ FILE: frontend/e2e/protractor.conf.js ================================================ // Protractor configuration file, see link for more information // https://github.com/angular/protractor/blob/master/lib/config.ts const { SpecReporter } = require('jasmine-spec-reporter'); exports.config = { allScriptsTimeout: 11000, specs: [ './src/**/*.e2e-spec.ts' ], capabilities: { 'browserName': 'chrome' }, directConnect: true, baseUrl: 'http://localhost:4200/', framework: 'jasmine', jasmineNodeOpts: { showColors: true, defaultTimeoutInterval: 30000, print: function() {} }, onPrepare() { require('ts-node').register({ project: require('path').join(__dirname, './tsconfig.e2e.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } }; ================================================ FILE: frontend/e2e/src/app.e2e-spec.ts ================================================ import { AppPage } from './app.po'; import { browser, logging } from 'protractor'; describe('workspace-project App', () => { let page: AppPage; beforeEach(() => { page = new AppPage(); }); it('should display welcome message', () => { page.navigateTo(); expect(page.getTitleText()).toEqual('Welcome to frontend!'); }); afterEach(async () => { // Assert that there are no errors emitted from the browser const logs = await browser.manage().logs().get(logging.Type.BROWSER); expect(logs).not.toContain(jasmine.objectContaining({ level: logging.Level.SEVERE, } as logging.Entry)); }); }); ================================================ FILE: frontend/e2e/src/app.po.ts ================================================ import { browser, by, element } from 'protractor'; export class AppPage { navigateTo() { return browser.get(browser.baseUrl) as Promise; } getTitleText() { return element(by.css('app-root h1')).getText() as Promise; } } ================================================ FILE: frontend/e2e/tsconfig.e2e.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "module": "commonjs", "target": "es6", "types": [ "jasmine", "jasminewd2", "node" ] } } ================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "version": "1.0.0", "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "start:prod": "ng serve -c production", "start:dev": "ng serve -c development", "build:prod": "ng build --configuration=production", "build:dev": "ng build --configuration=development", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { "@angular-devkit/build-angular": "~18.0.1", "@angular/animations": "~18.0.1", "@angular/cdk": "^18.0.1", "@angular/cli": "^18.0.1", "@angular/common": "~18.0.1", "@angular/compiler": "~18.0.1", "@angular/compiler-cli": "~18.0.1", "@angular/core": "~18.0.1", "@angular/forms": "~18.0.1", "@angular/platform-browser": "~18.0.1", "@angular/platform-browser-dynamic": "~18.0.1", "@angular/router": "~18.0.1", "@ng-select/ng-select": "^13.2.0", "chart.js": "^3.6.0", "core-js": "^3.19.0", "jwt-decode": "^3.1.2", "ngx-markdown": "^18.0.0", "primeflex": "^3.1.0", "primeicons": "^5.0.0", "primeng": "^17.18.0", "rxjs": "~7.8.1", "tslib": "^2.3.1", "zone.js": "~0.14.6" }, "devDependencies": { "@angular/language-service": "~18.0.1", "@types/jasmine": "~3.10.1", "@types/jasminewd2": "^2.0.10", "@types/node": "20.12.13", "codelyzer": "^6.0.2", "jasmine-core": "~4.0.1", "jasmine-spec-reporter": "~7.0.0", "karma": "~6.3.16", "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~3.0.2", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.7.0", "protractor": "~7.0.0", "ts-node": "~10.9.2", "tslint": "~6.1.0", "typescript": "~5.4.5" } } ================================================ FILE: frontend/src/app/admin.guard.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AdminGuard } from './admin.guard'; describe('AdminGuard', () => { let guard: AdminGuard; beforeEach(() => { TestBed.configureTestingModule({}); guard = TestBed.inject(AdminGuard); }); it('should be created', () => { expect(guard).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/admin.guard.ts ================================================ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router, } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; import { GlobalManagerService } from './global-manager.service'; @Injectable({ providedIn: 'root', }) export class AdminGuard implements CanActivate { constructor( private globalManager: GlobalManagerService, private router: Router, private authService: AuthService ) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | Observable | Promise | boolean | UrlTree { return this.checkLogin(); } checkLogin(): boolean { if (localStorage.getItem('AUTH_TOKEN')) { this.globalManager.showLogin(true); if (this.authService.isAdmin()) { return true; } else { return false; } } else { // Navigate to the login page with extras this.router.navigate(['/login']); this.globalManager.showLogin(false); return false; } } } ================================================ FILE: frontend/src/app/administration/administration.component.html ================================================
Administration



================================================ FILE: frontend/src/app/administration/administration.component.sass ================================================ h5 text-align: center ================================================ FILE: frontend/src/app/administration/administration.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AdministrationComponent } from './administration.component'; describe('AdministrationComponent', () => { let component: AdministrationComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AdministrationComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AdministrationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/administration/administration.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { InviteUserComponent } from './invite-user/invite-user.component'; import { SettingsComponent } from './settings/settings.component'; @Component({ selector: 'app-administration', templateUrl: './administration.component.html', styleUrls: ['./administration.component.sass'] }) export class AdministrationComponent implements OnInit { constructor() { } ngOnInit() { } } ================================================ FILE: frontend/src/app/administration/invite-user/invite-user.component.html ================================================
Invite a User

================================================ FILE: frontend/src/app/administration/invite-user/invite-user.component.sass ================================================ .centerBtn display: flex justify-content: center align-items: center height: 40px width: 75px border: 3px solid green .fill-width flex: 1 ================================================ FILE: frontend/src/app/administration/invite-user/invite-user.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { InviteUserComponent } from './invite-user.component'; describe('InviteUserComponent', () => { let component: InviteUserComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ InviteUserComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(InviteUserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/administration/invite-user/invite-user.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { UserService } from '../../user.service'; import { Router } from '@angular/router'; import { AlertService } from '../../alert/alert.service'; @Component({ selector: 'app-invite-user', templateUrl: './invite-user.component.html', styleUrls: ['./invite-user.component.sass'], }) export class InviteUserComponent implements OnInit { inviteForm: FormGroup; constructor( private fb: FormBuilder, public userService: UserService, public router: Router, public alertService: AlertService ) { this.createForm(); } ngOnInit(): void {} createForm() { this.inviteForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], }); } onSubmit(form) { const email = { email: form.value.email }; this.userService.inviteUser(email).subscribe((res: string) => { this.router.navigate(['administration']); this.alertService.success(res); }); } } ================================================ FILE: frontend/src/app/administration/settings/settings.component.html ================================================

Application Email Settings


An app password is a 16-digit passcode that gives a non-Google app or device permission to access your Google Account. Please follow this link for more information.

================================================ FILE: frontend/src/app/administration/settings/settings.component.sass ================================================ .centerBtn display: flex justify-content: center align-items: center height: 40px width: 75px border: 3px solid green ================================================ FILE: frontend/src/app/administration/settings/settings.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsComponent } from './settings.component'; describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ SettingsComponent ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(SettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/administration/settings/settings.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../../auth.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../../alert/alert.service'; import { Settings } from '../../interfaces/Settings'; import { UserService } from '../../user.service'; import { AppService } from '../../app.service'; @Component({ selector: 'app-settings', templateUrl: './settings.component.html', styleUrls: ['./settings.component.sass'], }) export class SettingsComponent implements OnInit { settingsForm: FormGroup; isEdit = false; settings: Settings; public keyPlaceholder = '************************'; constructor( private fb: FormBuilder, public authService: AuthService, public router: Router, public alertService: AlertService, public activatedRoute: ActivatedRoute, public userService: UserService, public appService: AppService ) {} ngOnInit(): void { this.activatedRoute.data.subscribe(({ settings }) => { this.createForm(); this.settings = settings; this.rebuildForm(); }); } createForm() { this.settingsForm = this.fb.group({ fromEmail: [{ value: '', disabled: !this.isEdit }], fromEmailPassword: [{ value: '', disabled: !this.isEdit }], companyName: [{ value: '', disabled: !this.isEdit }], }); } rebuildForm() { this.settingsForm.reset({ fromEmail: this.settings?.fromEmail, fromEmailPassword: this.settings?.fromEmailPassword, companyName: this.settings?.companyName, }); } onSubmit(form: FormGroup) { if (!this.isEdit) { this.isEdit = true; this.settingsForm.enable(); } else { const settingsInfo = form.value; this.appService.updateConfig(settingsInfo).subscribe((res: string) => { this.alertService.success(res); this.isEdit = false; this.settingsForm.disable(); }); } } } ================================================ FILE: frontend/src/app/alert/alert/alert.component.html ================================================
{{ alert.message }} ×
================================================ FILE: frontend/src/app/alert/alert/alert.component.sass ================================================ ================================================ FILE: frontend/src/app/alert/alert/alert.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AlertComponent } from './alert.component'; describe('AlertComponent', () => { let component: AlertComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AlertComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AlertComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/alert/alert/alert.component.ts ================================================ import { Component, OnInit, Input, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { Alert, AlertType } from 'src/app/classes/Alert'; import { AlertService } from '../alert.service'; @Component({ selector: 'app-alert', templateUrl: './alert.component.html', styleUrls: ['./alert.component.sass'] }) export class AlertComponent implements OnInit, OnDestroy { @Input() id: string; alerts: Alert[] = []; subscription: Subscription; constructor(private alertService: AlertService) {} ngOnInit() { this.subscription = this.alertService.onAlert(this.id).subscribe((alert) => { if (!alert.message) { // clear alerts when an empty alert is received this.alerts = []; return; } this.alerts.push(alert); }); } ngOnDestroy() { // unsubscribe to avoid memory leaks this.subscription.unsubscribe(); } removeAlert(alert: Alert) { // remove specified alert from array this.alerts = this.alerts.filter((x) => x !== alert); } cssClass(alert: Alert) { if (!alert) { return; } // return css class based on alert type switch (alert.type) { case AlertType.Success: return 'alert alert-success'; case AlertType.Error: return 'alert alert-danger'; case AlertType.Info: return 'alert alert-info'; case AlertType.Warning: return 'alert alert-warning'; } } } ================================================ FILE: frontend/src/app/alert/alert.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AlertComponent } from './alert/alert.component'; import { AlertService } from './alert.service'; @NgModule({ declarations: [AlertComponent], imports: [CommonModule], exports: [AlertComponent], providers: [AlertService] }) export class AlertModule {} ================================================ FILE: frontend/src/app/alert/alert.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AlertService } from './alert.service'; describe('AlertService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { const service: AlertService = TestBed.inject(AlertService); expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/alert/alert.service.ts ================================================ import { Injectable } from '@angular/core'; import { Router, NavigationStart } from '@angular/router'; import { Alert, AlertType } from '../classes/Alert'; import { Subject, Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class AlertService { private subject = new Subject(); private keepAfterRouteChange = false; constructor(private router: Router) { this.router.events.subscribe((event) => { if (event instanceof NavigationStart) { if (this.keepAfterRouteChange) { // only keep for a single route change this.keepAfterRouteChange = false; } else { // clear alert messages this.clear(); } } }); } // enable subscribing to alerts observable onAlert(alertId?: string): Observable { return this.subject.asObservable().pipe(filter((x) => x && x.alertId === alertId)); } // convenience methods success(message: string, alertId?: string) { this.alert(new Alert({ message, type: AlertType.Success, alertId })); } error(message: string, alertId?: string) { this.alert(new Alert({ message, type: AlertType.Error, alertId })); } info(message: string, alertId?: string) { this.alert(new Alert({ message, type: AlertType.Info, alertId })); } warn(message: string, alertId?: string) { this.alert(new Alert({ message, type: AlertType.Warning, alertId })); } clear(alertId?: string) { this.subject.next(new Alert({ alertId })); } // main alert method alert(alert: Alert) { this.keepAfterRouteChange = alert.keepAfterRouteChange; this.subject.next(alert); } } ================================================ FILE: frontend/src/app/apikey-management/apikey-management.component.html ================================================

API Key Management


User Email
Created Date
Last Updated Date
{{apiKey?.user?.email}} {{apiKey?.createdDate | date: 'longDate':'UTC' }} {{apiKey?.lastUpdatedDate | date: 'longDate':'UTC' }}
================================================ FILE: frontend/src/app/apikey-management/apikey-management.component.sass ================================================ ================================================ FILE: frontend/src/app/apikey-management/apikey-management.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ApikeyManagementComponent } from './apikey-management.component'; describe('ApikeyManagementComponent', () => { let component: ApikeyManagementComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ApikeyManagementComponent], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ApikeyManagementComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/apikey-management/apikey-management.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { AlertService } from '../alert/alert.service'; import { AuthService } from '../auth.service'; import { ApiKey } from '../interfaces/ApiKey'; @Component({ selector: 'app-apikey-management', templateUrl: './apikey-management.component.html', styleUrls: ['./apikey-management.component.sass'], }) export class ApikeyManagementComponent implements OnInit { apiKeys: ApiKey[] = []; constructor( public authService: AuthService, public alertService: AlertService ) {} ngOnInit(): void { this.getActiveApiKeys(); } getActiveApiKeys() { this.authService.getApiKeysInfo().subscribe((res: ApiKey[]) => { this.apiKeys = res; }); } deactivateApiKey(id: number) { const r = confirm( 'This API key may be in use by an external entity. Are you sure you want to delete it?' ); if (r) { this.authService.adminDeactivateApiKey(id).subscribe((res: string) => { this.alertService.success(res); this.getActiveApiKeys(); }); } } } ================================================ FILE: frontend/src/app/app-routing.module.ts ================================================ import { NgModule, Injectable } from '@angular/core'; import { Routes, RouterModule, Resolve, ActivatedRouteSnapshot, } from '@angular/router'; import { DashboardComponent } from '../app/dashboard/dashboard.component'; import { AssessmentsComponent } from '../app/assessments/assessments.component'; import { AppService } from '../app/app.service'; import { OrganizationComponent } from './organization/organization.component'; import { VulnerabilityComponent } from './vulnerability/vulnerability.component'; import { VulnFormComponent } from './vuln-form/vuln-form.component'; import { OrgFormComponent } from './org-form/org-form.component'; import { AssetFormComponent } from './asset-form/asset-form.component'; import { AssessmentFormComponent } from './assessment-form/assessment-form.component'; import { ReportComponent } from './report/report.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { AuthGuard } from './auth.guard'; import { AdminGuard } from './admin.guard'; import { LoginComponent } from './login/login.component'; import { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; import { PasswordResetComponent } from './password-reset/password-reset.component'; import { InviteUserComponent } from './administration/invite-user/invite-user.component'; import { AdministrationComponent } from './administration/administration.component'; import { TeamFormComponent } from './team-form/team-form.component'; import { RegisterComponent } from './register/register.component'; import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserFormComponent } from './user-form/user-form.component'; import { SettingsComponent } from './administration/settings/settings.component'; import { EmailValidateComponent } from './email-validate/email-validate.component'; import { UserService } from './user.service'; import { forkJoin } from 'rxjs'; import { map } from 'rxjs/internal/operators/map'; import { TeamService } from './team.service'; @Injectable() export class AssetsResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getOrganizationAssets(route.params.orgId); } } @Injectable() export class AssetResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getAsset(route.params.assetId, route.params.id); } } @Injectable() export class AssessmentResolver implements Resolve { constructor( private apiService: AppService, private userService: UserService ) {} resolve(route: ActivatedRouteSnapshot) { if ( route.params.assetId && route.params.assessmentId && route.params.orgId ) { return forkJoin([ this.apiService.getAssessment( route.params.assetId, route.params.assessmentId ), this.userService.getTesters(route.params.orgId), ]).pipe( map((result) => { return { assessment: result[0], testers: result[1], }; }) ); } else { return this.userService.getTesters(route.params.orgId); } } } @Injectable() export class AssessmentsResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getAssessments(route.params.assetId); } } @Injectable() export class VulnerabilitiesResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getVulnerabilities(route.params.assessmentId); } } @Injectable() export class VulnerabilityResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getVulnerability(route.params.vulnId); } } @Injectable() export class TeamResolver implements Resolve { constructor(private teamService: TeamService) {} resolve(route: ActivatedRouteSnapshot) { return this.teamService.getTeamById(route.params.teamId); } } @Injectable() export class TeamFormResolver implements Resolve { constructor( private appService: AppService, private userService: UserService ) {} resolve(route: ActivatedRouteSnapshot) { return forkJoin([ this.appService.getOrganizations(), this.userService.getAllUsers(), ]).pipe( map((result) => { return { organizations: result[0], activeUsers: result[1], }; }) ); } } @Injectable() export class OrganizationResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getOrganizationById(route.params.id); } } @Injectable() export class ReportResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getReport(route.params.assessmentId); } } @Injectable() export class UserResolver implements Resolve { constructor(private userService: UserService) {} resolve() { return this.userService.getUser(); } } @Injectable() export class SettingsResolver implements Resolve { constructor(private apiService: AppService) {} resolve(route: ActivatedRouteSnapshot) { return this.apiService.getConfig(); } } const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: 'login', component: LoginComponent, }, { path: 'forgot-password', component: ForgotPasswordComponent, }, { path: 'password-reset/:uuid', component: PasswordResetComponent, }, { path: 'register/:uuid', component: RegisterComponent, }, { path: 'email/validate/:uuid', component: EmailValidateComponent, }, { path: 'user/profile', component: UserProfileComponent, resolve: { user: UserResolver }, canActivate: [AuthGuard], }, { path: 'settings', component: SettingsComponent, resolve: { settings: SettingsResolver }, canActivate: [AuthGuard], }, { path: 'invite', component: InviteUserComponent, canActivate: [AuthGuard], }, { path: 'administration', component: AdministrationComponent, canActivate: [AdminGuard], }, { path: 'administration', component: AdministrationComponent, canActivate: [AdminGuard], }, { path: 'administration/team/:teamId', component: TeamFormComponent, canActivate: [AdminGuard], resolve: { result: TeamFormResolver }, }, { path: 'administration/team', component: TeamFormComponent, canActivate: [AdminGuard], resolve: { result: TeamFormResolver }, }, { path: 'administration/user/create', canActivate: [AdminGuard], component: UserFormComponent, }, { path: 'administration/user/invite', component: InviteUserComponent, canActivate: [AdminGuard], }, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId', component: AssessmentsComponent, resolve: { assessments: AssessmentsResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId', component: OrganizationComponent, resolve: { assets: AssetsResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId/vulnerability', component: VulnerabilityComponent, resolve: { vulnerabilities: VulnerabilitiesResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId/vuln-form/:vulnId', component: VulnFormComponent, resolve: { vulnInfo: VulnerabilityResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId/vuln-form', component: VulnFormComponent, canActivate: [AuthGuard], }, { path: 'organization-form', component: OrgFormComponent, canActivate: [AuthGuard], }, { path: 'organization-form/:id', component: OrgFormComponent, resolve: { organization: OrganizationResolver }, canActivate: [AuthGuard], }, { path: 'organization/:id/asset-form', component: AssetFormComponent, canActivate: [AuthGuard], }, { path: 'organization/:id/asset-form/:assetId', component: AssetFormComponent, resolve: { asset: AssetResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment', component: AssessmentFormComponent, resolve: { result: AssessmentResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId', component: AssessmentFormComponent, resolve: { result: AssessmentResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId/report', component: ReportComponent, resolve: { report: ReportResolver }, canActivate: [AuthGuard], }, { path: 'organization/:orgId/asset/:assetId/assessment/:assessmentId/report/puppeteer', component: ReportComponent, resolve: { report: ReportResolver }, canActivate: [AuthGuard], }, { path: '**', component: PageNotFoundComponent }, ]; @NgModule({ imports: [ RouterModule.forRoot(routes, { useHash: true, }), ], exports: [RouterModule], providers: [ AssetResolver, AssetsResolver, AssessmentsResolver, VulnerabilitiesResolver, OrganizationResolver, AssessmentResolver, VulnerabilityResolver, ReportResolver, UserResolver, SettingsResolver, TeamFormResolver, TeamResolver, ], }) export class AppRoutingModule {} ================================================ FILE: frontend/src/app/app.component.html ================================================
================================================ FILE: frontend/src/app/app.component.scss ================================================ ================================================ FILE: frontend/src/app/app.component.spec.ts ================================================ import { TestBed, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ RouterTestingModule ], declarations: [ AppComponent ], }).compileComponents(); })); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); }); it(`should have as title 'frontend'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app.title).toEqual('frontend'); }); it('should render title in a h1 tag', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!'); }); }); ================================================ FILE: frontend/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent {} ================================================ FILE: frontend/src/app/app.interceptor.ts ================================================ import { Injectable } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, } from '@angular/common/http'; import { Observable } from 'rxjs'; import { finalize, catchError, switchMap } from 'rxjs/operators'; import { LoaderService } from './loader.service'; import { AlertService } from './alert/alert.service'; import { AuthService } from './auth.service'; import { environment } from '../environments/environment'; import { Router } from '@angular/router'; import { Tokens } from './interfaces/Tokens'; @Injectable() export class AppInterceptor implements HttpInterceptor { constructor( public loaderService: LoaderService, public alertService: AlertService, public authService: AuthService, public router: Router ) {} logout() { this.authService.logout(); this.router.navigate(['login']); } intercept( req: HttpRequest, next: HttpHandler ): Observable> { const authToken = this.authService.getUserToken(); if (authToken) { req = req.clone({ headers: req.headers.set('Authorization', authToken), }); } this.loaderService.show(); return next.handle(req).pipe( finalize(() => this.loaderService.hide()), catchError((error: any) => { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. if (environment.production) { console.error('An error occurred:', error.error.message); } } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong, if (environment.production) { console.error( `Backend returned code ${error.status}, ` + `body was: ${error.error}` ); } switch (error.status) { case 500: error.error = 'Internal Server Error'; this.alertService.error(error.error); break; case 403: this.alertService.warn(error.error); break; case 404: this.alertService.warn(error.error); break; case 401: const url = environment.apiUrl; if (error.url === url + '/refresh') { this.logout(); this.alertService.error( 'You have been logged out due to inactivity' ); break; } return this.authService.refreshSession().pipe( switchMap((tokens: Tokens) => { this.authService.setTokens(tokens); req = req.clone({ headers: req.headers.set('Authorization', tokens.token), }); return next.handle(req); }) ); case 400: this.alertService.warn(error.error); break; default: error.error = 'Internal Server Error'; this.alertService.error(error.error); } } // return an observable with a user-facing error message return []; }) ); } } ================================================ FILE: frontend/src/app/app.module.ts ================================================ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { NavbarComponent } from './navbar/navbar.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { AlertModule } from './alert/alert.module'; import { NgSelectModule } from '@ng-select/ng-select'; import { AppService } from './app.service'; import { LoaderService } from './loader.service'; import { AuthGuard } from './auth.guard'; import { AppInterceptor } from './app.interceptor'; import { HttpClientModule } from '@angular/common/http'; import { AssessmentsComponent } from './assessments/assessments.component'; import { OrganizationComponent } from './organization/organization.component'; import { VulnerabilityComponent } from './vulnerability/vulnerability.component'; import { VulnFormComponent } from './vuln-form/vuln-form.component'; import { OrgFormComponent } from './org-form/org-form.component'; import { AssetFormComponent } from './asset-form/asset-form.component'; import { FooterComponent } from './footer/footer.component'; import { AssessmentFormComponent } from './assessment-form/assessment-form.component'; import { MarkdownModule } from 'ngx-markdown'; import { DatePipe } from '@angular/common'; import { ReportComponent } from './report/report.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { LoginComponent } from './login/login.component'; import { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; import { PasswordResetComponent } from './password-reset/password-reset.component'; import { InviteUserComponent } from './administration/invite-user/invite-user.component'; import { AdministrationComponent } from './administration/administration.component'; import { RegisterComponent } from './register/register.component'; import { UserProfileComponent } from './user-profile/user-profile.component'; import { SettingsComponent } from './administration/settings/settings.component'; import { TableModule } from 'primeng/table'; import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; import { CalendarModule } from 'primeng/calendar'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { ButtonModule } from 'primeng/button'; import { CardModule } from 'primeng/card'; import { PasswordModule } from 'primeng/password'; import { EmailValidateComponent } from './email-validate/email-validate.component'; import { ChartModule } from 'primeng/chart'; import { UserManagementComponent } from './user-management/user-management.component'; import { TeamComponent } from './team/team.component'; import { TeamFormComponent } from './team-form/team-form.component'; import { SelectButtonModule } from 'primeng/selectbutton'; import { ListboxModule } from 'primeng/listbox'; import { UserFormComponent } from './user-form/user-form.component'; import { ApikeyManagementComponent } from './apikey-management/apikey-management.component'; import { DialogModule } from 'primeng/dialog'; @NgModule({ declarations: [ AppComponent, NavbarComponent, DashboardComponent, OrganizationComponent, AssessmentsComponent, VulnerabilityComponent, OrgFormComponent, AssetFormComponent, VulnFormComponent, FooterComponent, AssessmentFormComponent, ReportComponent, PageNotFoundComponent, LoginComponent, ForgotPasswordComponent, PasswordResetComponent, InviteUserComponent, AdministrationComponent, RegisterComponent, UserProfileComponent, SettingsComponent, EmailValidateComponent, UserManagementComponent, TeamComponent, TeamFormComponent, UserFormComponent, ApikeyManagementComponent, ], imports: [ BrowserModule, BrowserAnimationsModule, AppRoutingModule, HttpClientModule, ReactiveFormsModule, MarkdownModule.forRoot(), AlertModule, NgSelectModule, TableModule, InputTextModule, MultiSelectModule, CalendarModule, ProgressSpinnerModule, ButtonModule, CardModule, ChartModule, PasswordModule, SelectButtonModule, ListboxModule, DialogModule, ], providers: [ AppService, DatePipe, LoaderService, { provide: HTTP_INTERCEPTORS, useClass: AppInterceptor, multi: true }, AuthGuard, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: frontend/src/app/app.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AppService } from './app.service'; describe('AppService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { const service: AppService = TestBed.inject(AppService); expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/app.service.ts ================================================ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Organization } from './org-form/Organization'; import { Asset } from './asset-form/Asset'; import { Assessment } from './assessment-form/Assessment'; import { DomSanitizer } from '@angular/platform-browser'; import { environment } from '../environments/environment'; import { User } from './interfaces/User'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AppService { constructor(private http: HttpClient, private sanitizer: DomSanitizer) {} api = environment.apiUrl; /** * Function is responsible for initial retrevial of organizations on dashboard loading * @returns all organization information to the dashboard */ getOrganizations() { return this.http .get(`${this.api}/organization`) .toPromise() .then(async (res) => { return res; }); } /** * Function responsible for retreval of organizations archived status. * @returns Data for organizations that have been archived. */ getArchivedOrganizations() { const httpOptions = { responseType: 'blob' as 'json', }; return this.http .get(`${this.api}/organization/archive`) .toPromise() .then(async (res) => { return res; }); } /** * Function is responsible for retreval of images as blogs using the ID association * @param file accepts the id assigned to a file * @returns the image associated with the id */ getImageById(file: any) { const httpOptions = { responseType: 'blob' as 'json', }; return this.http .get(`${this.api}/file/${file.id}`, httpOptions) .toPromise() .then((res: Blob) => { return this.createObjectUrl(res, file.mimetype); }); } /** * Function is responsible for retreving an organization based on ID passed * @param id is the ID of the organization being requested * @returns all information related to the organization requested */ getOrganizationById(id: number) { return this.http .get(`${this.api}/organization/${id}`) .toPromise() .then((res) => { return res; }); } /** * Function returns all active assets related to the organization ID * @param id is the ID of the organization * @returns all assets related to the organization passed */ getOrganizationAssets(id: number) { return this.http .get(`${this.api}/organization/asset/${id}`) .toPromise() .then((res) => { return res; }); } /** * Function returns all open vulnerabilities by Asset ID * @param assetId * @returns open asset vulnerabilities */ getOpenVulnsByAssetId(assetId: number) { return this.http.get(`${this.api}/asset/${assetId}/open/vulnerabilities`); } /** * Function returns all archived assets related to the organization ID * @param id is the ID of the organization * @returns all assets related to the organization passed */ getOrganizationArchiveAssets(id: number) { return this.http.get(`${this.api}/organization/${id}/asset/archive`); } /** * Function is responsible for archiving an organization by altering it's status * @param id is the organization being passed for archiving * @returns updates the status of the organization and reports the http status returned */ archiveOrganization(id: number) { return this.http.patch(`${this.api}/organization/${id}/archive`, null); } /** * Function is responsible for unarchving an organization by alterint it's status * @param id is the organization being passed for archiving * @returns updates the status of the organization and reports the http status returned */ activateOrganization(id: number) { return this.http.patch(`${this.api}/organization/${id}/activate`, null); } /** * Function is responsible for returning all assessements related to an organization * @param id is the organization ID associated with the assessements * @returns all assessments related to the organization */ getAssessments(id: number) { return this.http .get(`${this.api}/assessment/${id}`) .toPromise() .then((res) => { return res as object[]; }); } /** * Delete assessment by ID * @returns success/error message */ deleteAssessment(assessmentId: number) { return this.http.delete(`${this.api}/assessment/${assessmentId}`); } /** * Function is responsible for returning all vulnerabilites related to an assessment * @param assessmentId is the ID associated with the assessment * @returns all vulnerablities related to the assessment */ getVulnerabilities(assessmentId: number) { return this.http.get( `${this.api}/assessment/${assessmentId}/vulnerability` ); } /** * Function is responsible for returning a vulnerablity called by it's ID * @param id associated to the vulnerability requested * @returns all object related data to the vulnerability requested */ getVulnerability(id: number) { return this.http.get(`${this.api}/vulnerability/${id}`); } exportVulnToJira(vulnId: number) { return this.http.get(`${this.api}/vulnerability/jira/${vulnId}`); } exportAssessmentToJira(assessmentId: number) { return this.http.get(`${this.api}/assessment/jira/${assessmentId}`); } getConfig() { return this.http.get(`${this.api}/config`); } updateConfig(config: FormData) { return this.http.post(`${this.api}/config`, config); } /** * Function is responsible for updating a vulnerability by ID * @param id is associated with the requested vulnerability * @param vuln is associated with the form data passed as an object * @returns http status code for the return value */ updateVulnerability(id: number, vuln: FormData) { return this.http.patch(`${this.api}/vulnerability/${id}`, vuln); } /** * Function is responsible for creating a vulnerability for an assessment * @param vuln contains the form object data for all required fields * @returns http status code of the request */ createVuln(vuln: FormData) { return this.http.post(`${this.api}/vulnerability`, vuln); } /** * Function is responsible for deletion of a vulnerability * @param vulnId is the ID association to the vulnerability * @returns http status code of the request */ deleteVuln(vulnId: number) { return this.http.delete(`${this.api}/vulnerability/${vulnId}`); } /** * Function is responsible for creating a new organization * @param org object of the form data passed to the API * @returns http status code of the request */ createOrg(org: Organization) { return this.http.post(`${this.api}/organization`, org); } /** * Function is responsible for updating an organization * @param id is the organization ID being updated * @param org is the form object data passed to the API * @returns http status code of the request */ updateOrg(id: number, org: Organization) { return this.http.patch(`${this.api}/organization/${id}`, org); } /** * Function is responsible for creating a new asset tied to an organization * @param asset is the form object data for the new asset * @returns http status code of the request */ createAsset(asset: Asset) { return this.http.post( `${this.api}/organization/${asset.organization}/asset`, asset ); } purgeJira(assetId: number) { return this.http.delete(`${this.api}/asset/jira/${assetId}`); } /** * Function is responsible for fetching assets * @param assetId asset ID being requested * @param orgId associated organization ID attached to the asset * @returns https status code of the request */ getAsset(assetId: number, orgId: number) { return this.http.get(`${this.api}/organization/${orgId}/asset/${assetId}`); } /** * Function is responsible for updating an asset * @param asset is the ID associated to the asset * @returns http status code of the request */ updateAsset(asset: Asset) { return this.http.patch( `${this.api}/organization/${asset.organization}/asset/${asset.id}`, asset ); } /** * Function is responsible for archiving an asset * @param asset is the ID associated to the asset * @returns http status code of the request */ archiveAsset(asset: Asset) { return this.http.patch(`${this.api}/asset/archive/${asset.id}`, {}); } /** * Function is responsible for activating an asset * @param asset is the ID associated to the asset * @returns http status code of the request */ activateAsset(asset: Asset) { return this.http.patch(`${this.api}/asset/activate/${asset.id}`, {}); } /** * Function is responsible for creating new assessments * @param assessment data contained in the assessment form object * @returns http status code of the request */ createAssessment(assessment: Assessment) { return this.http.post(`${this.api}/assessment`, assessment); } /** * Function is responsible for updating an assessment's data * @param assessment form object data of the assessment * @param assessmentId associated ID of the assessment being altered * @param assetId asset ID attached to the request ties into the assessment ID * @returns http status code of the request */ updateAssessment( assessment: Assessment, assessmentId: number, assetId: number ) { return this.http.patch( `${this.api}/asset/${assetId}/assessment/${assessmentId}`, assessment ); } /** * Function is responsible for retrevial of assessments * @param assetId associated asset ID required * @param assessmentId associated assessment ID required * @returns http status code with object data from the API call */ getAssessment(assetId: number, assessmentId: number) { return this.http.get( `${this.api}/asset/${assetId}/assessment/${assessmentId}` ); } /** * Function is responsible for uploading files, attaching them to the resource requesting it * @param fileToUpload form object data for the files associated in the request * @returns http status code of the request */ upload(fileToUpload: File) { const formData: FormData = new FormData(); formData.append('file', fileToUpload); return this.http.post(`${this.api}/upload`, formData); } /** * Function is responsible for uploading multi-part data associated with files. * @param fileToUpload form object data holding the file objects required * @returns http status code of the request */ uploadMultiple(fileToUpload: FormData) { return this.http.post(`${this.api}/upload-multiple`, fileToUpload); } /** * Function is responsible for report retrevial * @param assessmentId required ID of the assessment for object data relations * @returns http status request and object data for the report */ getReport(assessmentId: number) { return this.http.get(`${this.api}/assessment/${assessmentId}/report`); } /** * Function is responsible for report generation * @param orgId requires associated data from the organization ID * @param assetId requires associated data from the asset ID * @param assessmentId requires associated data from the assessment ID * @returns http status code of the request along with a new tab with a generated report */ generateReport(orgId: number, assetId: number, assessmentId: number) { const httpOptions = { responseType: 'blob' as 'json', }; const generateObject = { orgId, assetId, assessmentId, }; return this.http.post( `${this.api}/report/generate`, generateObject, httpOptions ); } /** * Function is responsible for generating URL's to provide accessable data, reports, images, and * any other downloadble content. * @param file requires the file object to be called * @param [mimetype] requires the mimetype of the data * @returns new URL with the object requested in a sanatized manner */ public createObjectUrl(file, mimetype?: string) { // Preview unsaved form const blob = new Blob([file], { type: mimetype || file.type, }); const url = window.URL.createObjectURL(blob); return this.sanitizer.bypassSecurityTrustUrl(url); } } ================================================ FILE: frontend/src/app/assessment-form/Assessment.ts ================================================ import { URL } from 'url'; import { User } from '../interfaces/User'; export class Assessment { constructor( public id: number, public name: string, public executiveSummary: string, public asset: number, public jiraId: string, public testUrl: URL, public prodUrl: URL, public scope: string, public tag: number, public startDate: Date, public endDate: Date, public testers: User[] ) {} } ================================================ FILE: frontend/src/app/assessment-form/assessment-form.component.html ================================================
× {{item.firstName}} {{item.lastName}} {{item.firstName}} {{item.lastName}}
================================================ FILE: frontend/src/app/assessment-form/assessment-form.component.sass ================================================ ================================================ FILE: frontend/src/app/assessment-form/assessment-form.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AssessmentFormComponent } from './assessment-form.component'; describe('AssessmentFormComponent', () => { let component: AssessmentFormComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AssessmentFormComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AssessmentFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/assessment-form/assessment-form.component.ts ================================================ import { Component, OnInit, OnChanges } from '@angular/core'; import { Router, ActivatedRoute, RouterOutlet } from '@angular/router'; import { AppService } from '../app.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Assessment } from './Assessment'; import { AlertService } from '../alert/alert.service'; import { User } from '../interfaces/User'; @Component({ selector: 'app-assessment-form', templateUrl: './assessment-form.component.html', styleUrls: ['./assessment-form.component.sass'], }) export class AssessmentFormComponent implements OnInit, OnChanges { public assessmentModel: Assessment; public assessmentForm: FormGroup; public assetId: number; public assessmentId: number; public orgId: number; public testers: User[] = []; public readOnly: boolean; constructor( public appService: AppService, private fb: FormBuilder, public route: Router, public activatedRoute: ActivatedRoute, private alertService: AlertService ) { this.createForm(); } ngOnInit() { this.activatedRoute.data.subscribe(({ result }) => { if (result && result.assessment) { this.readOnly = result.assessment.readOnly; if (this.readOnly) { this.assessmentForm.disable(); } } if (result?.assessment?.assessment) { result.assessment.assessment.startDate = this.transformDate( result.assessment.assessment.startDate ); result.assessment.assessment.endDate = this.transformDate( result.assessment.assessment.endDate ); this.testers = result.testers; this.assessmentForm.patchValue(result.assessment.assessment); } else { this.testers = result; } }); this.activatedRoute.params.subscribe((params) => { this.assetId = +params.assetId; this.assessmentId = +params.assessmentId; this.orgId = +params.orgId; }); } /** * Function responsible to detect changes for the form and rebuild it */ ngOnChanges() { this.rebuildForm(); } /** * Function responsible for formatting the date data in the form for * storage * @param date is the date data from the form * @returns formatted date to be stored */ transformDate(date: string) { return date.substring(0, 10); } /** * Function responsible for rebuilding the reactive form in Angular */ rebuildForm() { this.assessmentForm.reset({ name: this.assessmentModel.name, executiveSummary: this.assessmentModel.executiveSummary, jiraId: this.assessmentModel.jiraId, testUrl: this.assessmentModel.testUrl, prodUrl: this.assessmentModel.prodUrl, scope: this.assessmentModel.scope, tag: this.assessmentModel.tag, startDate: this.assessmentModel.startDate, endDate: this.assessmentModel.endDate, testers: this.assessmentModel.testers, }); } /** * Function responsible for creating the reactive form in Angular */ createForm() { this.assessmentForm = this.fb.group({ name: ['', [Validators.required]], executiveSummary: ['', Validators.maxLength(4000)], jiraId: ['', []], testUrl: ['', [Validators.required]], prodUrl: ['', [Validators.required]], scope: ['', [Validators.required]], tag: ['', []], startDate: ['', [Validators.required]], endDate: ['', [Validators.required]], testers: ['', [Validators.required]], }); } /** * Function responsible for handling the form submission and triggers a call * to the createOrUpdateAssessment() function */ onSubmit(assessment: FormGroup) { this.assessmentModel = assessment.value; this.createOrUpdateAssessment(this.assessmentModel); } /** * Function responsible for handling the form data and creating or updating * an Assessment * @param assessment form object data passed from OnSubmit() */ createOrUpdateAssessment(assessment: Assessment) { if (this.assessmentId) { this.appService .updateAssessment(assessment, this.assessmentId, this.assetId) .subscribe((res: string) => { this.navigateToAssessments(); this.alertService.success(res); }); } else { this.assessmentModel.asset = this.assetId; this.appService .createAssessment(this.assessmentModel) .subscribe((res: string) => { this.navigateToAssessments(); this.alertService.success(res); }); } } /** * Function responsible for navigating the user back to the Assessments View */ navigateToAssessments() { this.route.navigate([`organization/${this.orgId}/asset/${this.assetId}`]); } } ================================================ FILE: frontend/src/app/assessments/assessments.component.html ================================================
{{col.header}} {{ assessment.id }} {{ assessment?.name }}
{{tester?.firstName}} {{tester?.lastName}},
{{ assessment?.jiraId }} {{ assessment?.startDate | date: "shortDate":'UTC' }} {{ assessment?.endDate | date: "shortDate":'UTC' }}
================================================ FILE: frontend/src/app/assessments/assessments.component.sass ================================================ ================================================ FILE: frontend/src/app/assessments/assessments.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AssessmentsComponent } from './assessments.component'; describe('AssessmentsComponent', () => { let component: AssessmentsComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AssessmentsComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AssessmentsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/assessments/assessments.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { FilterService, FilterMatchMode } from 'primeng/api'; import { AppService } from '../app.service'; import { AlertService } from '../alert/alert.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Assessment } from '../assessment-form/Assessment'; import { Table } from 'primeng/table'; import { User } from '../interfaces/User'; import { UserService } from '../user.service'; // User decorated with compound name field interface FormattedUser extends User { name: string; } @Component({ selector: 'app-assessments', templateUrl: './assessments.component.html', styleUrls: ['./assessments.component.sass'], }) export class AssessmentsComponent implements OnInit { assessmentAry: any = []; assetId: number; orgId: number; testers: FormattedUser[]; readOnly: boolean; @ViewChild('assessmentTable') table: Table; cols = [ { field: 'id', filterMatchMode: FilterMatchMode.CONTAINS, header: 'Assessment ID', }, { field: 'name', filterMatchMode: FilterMatchMode.CONTAINS, header: 'Assessment Name', }, { field: 'testers', filterMatchMode: 'arrayCompare', header: 'Testers', }, { field: 'jiraId', filterMatchMode: FilterMatchMode.CONTAINS, header: 'Jira ID', }, { field: 'startDate', filterMatchMode: FilterMatchMode.EQUALS, header: 'Start Date', }, { field: 'startDate', filterMatchMode: FilterMatchMode.EQUALS, header: 'End Date', }, ]; constructor( public activatedRoute: ActivatedRoute, public router: Router, public appService: AppService, public alertService: AlertService, public userService: UserService, private filterService: FilterService ) {} ngOnInit() { this.activatedRoute.data.subscribe(({ assessments }) => { this.readOnly = assessments.readOnly; this.assessmentAry = assessments.assessments; }); this.activatedRoute.params.subscribe((params) => { this.assetId = params.assetId; this.orgId = params.orgId; }); this.userService.getUsers().subscribe((testers) => { // add a composite 'name' field to the Testers to display in MultiSelect this.testers = testers.map((tester) => ({ ...tester, name: this.formatName(tester), })); }); this.addArrayCompareTableFilter(); } /** * Create custom table filter "matchMode" to compare multiselect filter array values to array of values in a table row field */ private addArrayCompareTableFilter() { this.filterService.register( 'arrayCompare', (values: User[], selections: FormattedUser[]): boolean => { return values.some((value) => { return !!selections.some( (selection) => selection.name === this.formatName(value) ); }); } ); } /** * Function responsible for directing the user to the vulnerability details * @param id of vulnerability to load */ navigateToVulnerability(id: number) { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${id}/vulnerability`, ]); } /** * Function responsible for directing the user back to the assessments view * passes organization id to fetch data */ navigateToOrganization() { this.router.navigate([`organization/${this.orgId}`]); } /** * Function responsible for directing the user to the main Assessment view */ navigateToAssessment() { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment`, ]); } /** * Function responsible for directing the user to an assessment view with provided * ID * @param assessmentId is the ID associated to the assessment to load */ navigateToAssessmentById(assessmentId: number) { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${assessmentId}`, ]); } /** * Delete assessment by ID * ID * @param assessmentId is the ID associated to the assessment to load */ deleteAssessment(assessment: Assessment) { const r = confirm(`Delete the assessment "${assessment.name}"`); if (r === true) { this.appService .deleteAssessment(assessment.id) .subscribe((success: string) => { this.alertService.success(success); this.appService .getAssessments(this.orgId) .then((res) => (this.assessmentAry = res)); }); } } onDateSelect(value, type) { const date = new Date(value); date.setUTCHours(0, 0, 0, 0); if (type === 'startDate') { this.table.filter(date.toISOString(), 'startDate', 'equals'); } else if (type === 'endDate') { this.table.filter(date.toISOString(), 'endDate', 'equals'); } } /** * Format composite name from first and last name fields */ private formatName(user) { return `${user.firstName} ${user.lastName}`; } } ================================================ FILE: frontend/src/app/asset-form/Asset.ts ================================================ import { Jira } from './Jira'; export interface Asset { id: number; name: string; organization: number; jira?: Jira; } ================================================ FILE: frontend/src/app/asset-form/Jira.ts ================================================ export interface Jira { id?: number; username?: string; host?: string; url?: string; apiKey?: string; } ================================================ FILE: frontend/src/app/asset-form/asset-form.component.html ================================================
Jira Integration


================================================ FILE: frontend/src/app/asset-form/asset-form.component.sass ================================================ ================================================ FILE: frontend/src/app/asset-form/asset-form.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AssetFormComponent } from './asset-form.component'; describe('AssetFormComponent', () => { let component: AssetFormComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AssetFormComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AssetFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/asset-form/asset-form.component.ts ================================================ import { Component, OnInit, OnChanges } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Asset } from './Asset'; import { AppService } from '../app.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-asset-form', templateUrl: './asset-form.component.html', styleUrls: ['./asset-form.component.sass'], }) export class AssetFormComponent implements OnInit, OnChanges { public assetModel: Asset; public assetForm: FormGroup; public orgId: number; public assetId: number; public keyPlaceholder = '************************'; public canAddApiKey = true; public isAdmin: boolean; constructor( private fb: FormBuilder, public appService: AppService, public route: Router, public activatedRoute: ActivatedRoute, private alertService: AlertService, public authService: AuthService ) { this.createForm(); } ngOnInit() { this.activatedRoute.data.subscribe(({ asset }) => { this.isAdmin = this.authService.isAdmin(); if (asset) { this.assetModel = asset; if (!this.isAdmin) { this.assetForm.disable(); } this.rebuildForm(); if (asset.jira) { this.canAddApiKey = false; } else { this.canAddApiKey = true; } } }); this.activatedRoute.params.subscribe((params) => { this.orgId = params.id; this.assetId = params.assetId; }); } /** * Function responsible to detect changes for the form and rebuild it */ ngOnChanges() { this.rebuildForm(); } /** * Function responsible for creating the reactive form in Angular */ createForm() { this.assetForm = this.fb.group({ name: ['', [Validators.required]], jira: this.fb.group({ username: ['', []], host: ['', []], apiKey: ['', []], }), }); } /** * Function responsible for rebuilding the reactive form in Angular */ rebuildForm() { this.assetForm.reset({ name: this.assetModel.name, jira: { username: this.assetModel?.jira?.username, host: this.assetModel?.jira?.host, apiKey: this.assetModel?.jira?.apiKey, }, }); } /** * Function responsible for processing the data from the reactive from * @param asset data object holding all the form data */ onSubmit(asset: FormGroup) { this.assetModel = asset.value; this.assetModel.organization = this.orgId; this.assetModel.id = this.assetId; if (!this.canAddApiKey) { this.assetModel.jira = null; } this.createOrUpdateAsset(this.assetModel); } /** * Function responsible for sending the user back to Assets listing */ navigateToAssets() { this.route.navigate([`organization/${this.orgId}`]); } purgeJiraInfo() { const r = confirm(`Purge API Key for Asset: "${this.assetModel.name}"?`); if (r) { this.appService.purgeJira(this.assetId).subscribe((res: string) => { this.alertService.success(res); this.appService .getAsset(this.assetId, this.orgId) .subscribe((asset: Asset) => { this.assetModel = asset; this.canAddApiKey = true; this.rebuildForm(); }); }); } } /** * Function responsible for creating or updating an asset tied to * an organization * @param asset object holding all the asset data */ createOrUpdateAsset(asset: Asset) { if (this.assetId) { this.appService.updateAsset(asset).subscribe((res: string) => { this.navigateToAssets(); this.alertService.success(res); }); } else { this.appService.createAsset(asset).subscribe((res: string) => { this.navigateToAssets(); this.alertService.success(res); }); } } } ================================================ FILE: frontend/src/app/auth.guard.spec.ts ================================================ import { TestBed, inject, waitForAsync } from '@angular/core/testing'; import { AuthGuard } from './auth.guard'; describe('AuthGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [AuthGuard] }); }); it('should ...', inject([AuthGuard], (guard: AuthGuard) => { expect(guard).toBeTruthy(); })); }); ================================================ FILE: frontend/src/app/auth.guard.ts ================================================ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; import { GlobalManagerService } from './global-manager.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private globalManager: GlobalManagerService, private router: Router ) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | Observable | Promise | boolean | UrlTree { return this.checkLogin(); } checkLogin(): boolean { if (localStorage.getItem('AUTH_TOKEN')) { this.globalManager.showLogin(true); return true; } else { // Navigate to the login page with extras this.router.navigate(['/login']); this.globalManager.showLogin(false); return false; } } } ================================================ FILE: frontend/src/app/auth.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AuthService } from './auth.service'; describe('AuthService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { const service: AuthService = TestBed.inject(AuthService); expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/auth.service.ts ================================================ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../environments/environment'; import { Tokens } from './interfaces/Tokens'; import jwt_decode from 'jwt-decode'; @Injectable({ providedIn: 'root', }) export class AuthService { constructor(private http: HttpClient) {} api = environment.apiUrl; isLoggedIn = false; login(creds) { return this.http.post(`${this.api}/login`, creds); } logout() { localStorage.removeItem('REFRESH_TOKEN'); localStorage.removeItem('AUTH_TOKEN'); } setTokens(tokens: Tokens) { localStorage.setItem('AUTH_TOKEN', tokens.token); localStorage.setItem('REFRESH_TOKEN', tokens.refreshToken); } getUserToken() { return localStorage.getItem('AUTH_TOKEN'); } getUserFromToken() { return jwt_decode(localStorage.getItem('AUTH_TOKEN')); } isAdmin() { const token = this.getUserFromToken(); let found = false; // tslint:disable-next-line: no-string-literal found = token['admin']; return found; } getRefreshToken() { return localStorage.getItem('REFRESH_TOKEN'); } forgotPassword(email) { return this.http.patch(`${this.api}/forgot-password`, email); } passwordReset(creds) { return this.http.patch(`${this.api}/password-reset`, creds); } updatePassword( oldPassword: string, newPassword: string, confirmNewPassword: string ) { return this.http.patch(`${this.api}/user/password`, { oldPassword, newPassword, confirmNewPassword, }); } updateUserEmail(email: string, newEmail: string) { return this.http.post(`${this.api}/user/email`, { email, newEmail, }); } validateUserEmailRequest(password: string, uuid: string) { return this.http.post(`${this.api}/user/email/validate`, { password, uuid, }); } revokeUserEmail() { return this.http.post(`${this.api}/user/email/revoke`, null); } refreshSession() { const refreshToken = this.getRefreshToken(); return this.http.post(`${this.api}/refresh`, { refreshToken }); } generateApiKey() { return this.http.post(`${this.api}/user/key`, null); } getApiKeyInfo() { return this.http.get(`${this.api}/user/key`); } getApiKeysInfo() { return this.http.get(`${this.api}/keys`); } deactivateApiKey(id: number) { return this.http.patch(`${this.api}/user/key/${id}`, null); } adminDeactivateApiKey(id: number) { return this.http.patch(`${this.api}/key/${id}`, null); } } ================================================ FILE: frontend/src/app/classes/Alert.ts ================================================ export class Alert { type: AlertType; message: string; alertId: string; keepAfterRouteChange: boolean; constructor(init?: Partial) { Object.assign(this, init); } } export enum AlertType { Success, Error, Info, Warning } ================================================ FILE: frontend/src/app/classes/ProblemLocation.ts ================================================ export class ProblemLocation { constructor(private id: number, public location: string, public target: string) {} } ================================================ FILE: frontend/src/app/classes/Resource.ts ================================================ export class Resource { constructor(private id: number, public resURL: string) {} } ================================================ FILE: frontend/src/app/dashboard/dashboard.component.html ================================================
Organization ID Organization Name {{org.id}} {{org.name}}
================================================ FILE: frontend/src/app/dashboard/dashboard.component.sass ================================================ .card-img-top padding: 20px ================================================ FILE: frontend/src/app/dashboard/dashboard.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { DashboardComponent } from './dashboard.component'; describe('DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ DashboardComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/dashboard.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { AppService } from '../app.service'; import { ActivatedRoute, Router } from '@angular/router'; import { Organization } from '../org-form/Organization'; import { AlertService } from '../alert/alert.service'; import { Table } from 'primeng/table'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.sass'], }) export class DashboardComponent implements OnInit { orgAry: any = []; assetAry: any = []; orgId: number; isArchive = false; isAdmin: boolean; @ViewChild('orgTable') table: Table; constructor( private appService: AppService, public activatedRoute: ActivatedRoute, public router: Router, private alertService: AlertService, public authService: AuthService ) {} ngOnInit() { this.getOrganizations(); } /** * Function responsible for retreving all organizational data */ getOrganizations() { this.appService.getOrganizations().then((res) => { this.isArchive = false; this.orgAry = res; this.isAdmin = this.authService.isAdmin(); }); } /** * Function responsible for checking the status of an organization * to determine if it is archived or not */ getArchivedOrganizations() { this.appService.getArchivedOrganizations().then((res) => { this.isArchive = true; this.orgAry = res; }); } /** * Function responsible for navigating to assests tied to an organization * @param id is the ID of the organization tied to the assets */ navigateToAsset(id: number) { this.router.navigate([`organization/${id}`]); } /** * Function responsible for navigating the user to the organization form to * either create or update an organization */ navigateToCreate() { this.router.navigate([`organization-form`]); } /** * Function responsible for loading the organization the end user selects * @param id is the associated ID of the organization requested */ navigateToOrganization(id: number) { this.router.navigate([`organization-form/${id}`]); } /** * Function responsible for archiving an organization by * toggling the associated status * @param org is the ID of the organization to alter */ archiveOrganization(org: Organization) { const confirmed = confirm(`Archive the organization "${org.name}"?`); if (confirmed) { this.appService.archiveOrganization(org.id).subscribe((res: string) => { this.getOrganizations(); this.alertService.success(res); }); } } /** * Function responsible for altering an organization status back * to active * @param org is the ID of the organization to alter */ activateOrganization(org: Organization) { const confirmed = confirm(`Activate the organization "${org.name}"?`); if (confirmed) { this.appService.activateOrganization(org.id).subscribe((res: string) => { this.getOrganizations(); this.alertService.success(res); }); } } } ================================================ FILE: frontend/src/app/email-validate/email-validate.component.html ================================================

Email Update Validation


Please enter your password to validate the email update request
================================================ FILE: frontend/src/app/email-validate/email-validate.component.sass ================================================ ================================================ FILE: frontend/src/app/email-validate/email-validate.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { EmailValidateComponent } from './email-validate.component'; describe('EmailValidateComponent', () => { let component: EmailValidateComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ EmailValidateComponent ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(EmailValidateComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/email-validate/email-validate.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; @Component({ selector: 'app-email-validate', templateUrl: './email-validate.component.html', styleUrls: ['./email-validate.component.sass'], }) export class EmailValidateComponent implements OnInit { uuid: string; emailUpdateForm: FormGroup; constructor( private activatedRoute: ActivatedRoute, private fb: FormBuilder, public authService: AuthService, public router: Router, public alertService: AlertService ) { this.uuid = this.activatedRoute.snapshot.paramMap.get('uuid'); this.createForm(); } ngOnInit(): void {} createForm() { this.emailUpdateForm = this.fb.group({ password: ['', Validators.required], }); } onSubmit(form) { this.authService .validateUserEmailRequest(form.value.password, this.uuid) .subscribe((res: string) => { this.router.navigate(['login']); this.alertService.success(res); }); } } ================================================ FILE: frontend/src/app/enums/roles.enum.ts ================================================ export const ROLE = { ADMIN: 'Admin', READONLY: 'Read-Only', TESTER: 'Tester', }; ================================================ FILE: frontend/src/app/footer/footer.component.html ================================================
================================================ FILE: frontend/src/app/footer/footer.component.sass ================================================ ================================================ FILE: frontend/src/app/footer/footer.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { FooterComponent } from './footer.component'; describe('FooterComponent', () => { let component: FooterComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ FooterComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(FooterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/footer/footer.component.ts ================================================ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-footer', templateUrl: './footer.component.html', styleUrls: ['./footer.component.sass'] }) export class FooterComponent implements OnInit { constructor() { } ngOnInit() { } } ================================================ FILE: frontend/src/app/forgot-password/forgot-password.component.html ================================================ ================================================ FILE: frontend/src/app/forgot-password/forgot-password.component.sass ================================================ ================================================ FILE: frontend/src/app/forgot-password/forgot-password.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ForgotPasswordComponent } from './forgot-password.component'; describe('ForgotPasswordComponent', () => { let component: ForgotPasswordComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ ForgotPasswordComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ForgotPasswordComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/forgot-password/forgot-password.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { AlertService } from '../alert/alert.service'; @Component({ selector: 'app-forgot-password', templateUrl: './forgot-password.component.html', styleUrls: ['./forgot-password.component.sass'], }) export class ForgotPasswordComponent implements OnInit { pwdResetForm: FormGroup; constructor( private fb: FormBuilder, public authService: AuthService, public router: Router, public alertService: AlertService ) { this.createForm(); } ngOnInit(): void {} createForm() { this.pwdResetForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], }); } onSubmit(form) { const email = { email: form.value.email }; this.authService.forgotPassword(email).subscribe((res: string) => { this.alertService.success(res); this.pwdResetForm.reset(); }); } } ================================================ FILE: frontend/src/app/global-manager.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { GlobalManagerService } from './global-manager.service'; describe('GlobalManagerService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { const service: GlobalManagerService = TestBed.inject(GlobalManagerService); expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/global-manager.service.ts ================================================ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class GlobalManagerService { private loggedIn: BehaviorSubject = new BehaviorSubject( null ); public loggedIn$: Observable = this.loggedIn.asObservable(); constructor() {} showLogin(ifShow: boolean) { this.loggedIn.next(ifShow); } } ================================================ FILE: frontend/src/app/interfaces/ApiKey.ts ================================================ export interface ApiKey { id: number; createdDate: Date; lastUpdatedDate: Date; active?: boolean; email?: boolean; } ================================================ FILE: frontend/src/app/interfaces/App_File.ts ================================================ import { SafeUrl } from '@angular/platform-browser'; export interface AppFile extends File { id: number; buffer: any; encoding: string; fieldName: string; mimetype: string; originalname: string; size: number; imgUrl: SafeUrl; } ================================================ FILE: frontend/src/app/interfaces/Organization.ts ================================================ export interface Organization { id: number; name: string; status: string; } ================================================ FILE: frontend/src/app/interfaces/Screenshot.ts ================================================ import { SafeUrl } from '@angular/platform-browser'; export interface Screenshot { url: SafeUrl; file: any; fileName: string; fileId: number; } ================================================ FILE: frontend/src/app/interfaces/Settings.ts ================================================ export interface Settings { fromEmail?: string; fromEmailPassword?: string; companyName?: string; } ================================================ FILE: frontend/src/app/interfaces/Team.ts ================================================ import { Organization } from '../org-form/Organization'; import { Asset } from '../asset-form/Asset'; interface Role { name: string; } export interface Team { id?: number; name: string; organization: Organization; assets?: Asset[]; assetIds?: number[]; role: any; users: number[]; } ================================================ FILE: frontend/src/app/interfaces/Tokens.ts ================================================ export interface Tokens { token: string; refreshToken: string; } ================================================ FILE: frontend/src/app/interfaces/User.ts ================================================ import { Team } from './Team'; export interface User { firstName: string; lastName: string; title: string; password?: string; confirmPassword?: string; email?: string; newEmail?: string; id?: string; active?: boolean; teams?: Team[]; } ================================================ FILE: frontend/src/app/loader.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { LoaderService } from './loader.service'; describe('LoaderService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { const service: LoaderService = TestBed.inject(LoaderService); expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/loader.service.ts ================================================ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Subject } from 'rxjs/internal/Subject'; @Injectable({ providedIn: 'root' }) export class LoaderService { isLoading = new Subject(); show() { this.isLoading.next(true); } hide() { this.isLoading.next(false); } constructor() {} } ================================================ FILE: frontend/src/app/login/login.component.html ================================================ ================================================ FILE: frontend/src/app/login/login.component.sass ================================================ ================================================ FILE: frontend/src/app/login/login.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { LoginComponent } from './login.component'; describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ LoginComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/login/login.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { Tokens } from '../interfaces/Tokens'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.sass'], }) export class LoginComponent implements OnInit { loginForm: FormGroup; submitted = false; constructor( private fb: FormBuilder, public authService: AuthService, public router: Router ) { this.createForm(); } ngOnInit() {} createForm() { this.loginForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], password: ['', Validators.required], }); } onSubmit(creds) { const login = { email: creds.value.email, password: creds.value.password }; this.authService.login(login).subscribe((tokens: Tokens) => { this.authService.setTokens(tokens); this.router.navigate(['dashboard']); }); } } ================================================ FILE: frontend/src/app/navbar/navbar.component.html ================================================ ================================================ FILE: frontend/src/app/navbar/navbar.component.sass ================================================ .dropdown-menu left: -50px margin: .125rem -10px 0 ================================================ FILE: frontend/src/app/navbar/navbar.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NavbarComponent } from './navbar.component'; describe('NavbarComponent', () => { let component: NavbarComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ NavbarComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavbarComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/navbar/navbar.component.ts ================================================ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { LoaderService } from '../loader.service'; import { AuthService } from '../auth.service'; import { GlobalManagerService } from '../global-manager.service'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', styleUrls: ['./navbar.component.sass'], changeDetection: ChangeDetectionStrategy.OnPush }) export class NavbarComponent implements OnInit { loggedIn$; constructor( public loaderService: LoaderService, public authService: AuthService, private globalManager: GlobalManagerService ) { this.loggedIn$ = this.globalManager.loggedIn$; } ngOnInit() {} logout() { this.authService.logout(); } } ================================================ FILE: frontend/src/app/org-form/Organization.ts ================================================ export class Organization { constructor(public id: number, public name: string) {} } ================================================ FILE: frontend/src/app/org-form/org-form.component.html ================================================
================================================ FILE: frontend/src/app/org-form/org-form.component.sass ================================================ ================================================ FILE: frontend/src/app/org-form/org-form.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { OrgFormComponent } from './org-form.component'; describe('OrgFormComponent', () => { let component: OrgFormComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ OrgFormComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(OrgFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/org-form/org-form.component.ts ================================================ import { Component, OnInit, OnChanges } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Organization } from './Organization'; import { AppService } from '../app.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-org-form', templateUrl: './org-form.component.html', styleUrls: ['./org-form.component.sass'], }) export class OrgFormComponent implements OnInit, OnChanges { public orgModel: Organization; orgForm: FormGroup; fileToUpload: File = null; orgId: number; isAdmin: boolean; constructor( private fb: FormBuilder, public appService: AppService, public route: Router, public activatedRoute: ActivatedRoute, private alertService: AlertService, public authService: AuthService ) { this.createForm(); } ngOnInit() { this.activatedRoute.data.subscribe(({ organization }) => { this.isAdmin = this.authService.isAdmin(); if (organization) { this.orgModel = organization; if (!this.isAdmin) { this.disableForm(); } this.rebuildForm(); } }); this.activatedRoute.params.subscribe((params) => { this.orgId = params.id; }); } ngOnChanges() { this.rebuildForm(); } /** * Function required to create the active form in Angular */ createForm() { this.orgForm = this.fb.group({ name: ['', [Validators.required]], }); } /** * Function required to rebuild the form on changes in Angular */ rebuildForm() { this.orgForm.reset({ name: this.orgModel.name, }); } disableForm() { this.orgForm.disable(); } /** * Function required to process the files attached to the form * @param files array of files to work with */ handleFileInput(files: FileList) { this.fileToUpload = files.item(0); } /** * Function required to process the form data * @param contact form data object holding organization data */ onSubmit(contact: FormGroup) { this.orgModel = contact.value; if (this.fileToUpload) { this.appService.upload(this.fileToUpload).subscribe((fileId) => { this.createOrUpdateOrg(this.orgModel); }); } else { this.createOrUpdateOrg(this.orgModel); } } /** * Function required to create or update an organization based on org ID * navigates the user back to the main dashboard after action is executed * @param org contains organization data object */ createOrUpdateOrg(org: Organization) { if (this.orgId) { this.appService .updateOrg(this.orgId, org) .subscribe((success: string) => { this.navigateToDashboard(); this.alertService.success(success); }); } else { this.appService.createOrg(org).subscribe((success: string) => { this.navigateToDashboard(); this.alertService.success(success); }); } } /** * Function responsible for directing the user back to the main dashboard */ navigateToDashboard() { this.route.navigate(['dashboard']); } } ================================================ FILE: frontend/src/app/organization/organization.component.html ================================================
Asset ID Asset Name Jira Enabled Status # Open Vulnerabilities {{ asset?.id }} {{ asset?.name }} {{asset?.jira?.id ? 'Yes' : 'No'}} {{ asset?.status === 'A' ? 'Active':'Archived' }} {{asset?.openVulnCount}}
ID Name Risk Systemic CVSS Score Jira ID
{{option.name}}
{{vuln?.id}} {{vuln?.name}} {{vuln?.risk}} {{vuln?.systemic}} {{vuln?.cvssScore}} {{vuln?.jiraId}}
================================================ FILE: frontend/src/app/organization/organization.component.sass ================================================ ================================================ FILE: frontend/src/app/organization/organization.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { OrganizationComponent } from './organization.component'; describe('OrganizationComponent', () => { let component: OrganizationComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ OrganizationComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(OrganizationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/organization/organization.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AppService } from '../app.service'; import { Asset } from '../asset-form/Asset'; import { AlertService } from '../alert/alert.service'; import { Table } from 'primeng/table'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-organization', templateUrl: './organization.component.html', styleUrls: ['./organization.component.sass'], }) export class OrganizationComponent implements OnInit { assetAry: any = []; orgId: number; assetId: number; org: any; isArchive = false; isAdmin: boolean; displayOpenVulnModal = false; openVulns: any = []; assetNameHeader: string; @ViewChild('dt') table: Table; @ViewChild('vulnTable') vulnTable: Table; risks = [ { name: 'Informational' }, { name: 'Low' }, { name: 'Medium' }, { name: 'High' }, { name: 'Critical' }, ]; constructor( public activatedRoute: ActivatedRoute, public router: Router, public appService: AppService, public alertService: AlertService, public authService: AuthService ) {} ngOnInit() { this.activatedRoute.data.subscribe(({ assets }) => { this.assetAry = assets; this.isAdmin = this.authService.isAdmin(); }); this.activatedRoute.params.subscribe((params) => { this.orgId = params.orgId; this.appService .getOrganizationById(this.orgId) .then((org) => (this.org = org)); }); } onRepresentativeChange(event) { this.table.filter(event.value, 'name', 'in'); } /** * Function responsible retrieving active assets */ getActiveAssets() { this.appService.getOrganizationAssets(this.orgId).then((activeAssets) => { this.assetAry = activeAssets; this.isArchive = false; }); } /** * Function responsible retrieving archived assets */ getArchivedAssets() { this.appService .getOrganizationArchiveAssets(this.orgId) .subscribe((archivedAssets) => { this.assetAry = archivedAssets; this.isArchive = true; }); } /** * Function responsible for navigating the user to an Assessment * @param id assessment ID is required */ navigateToAssessment(id: number) { this.router.navigate([`organization/${this.orgId}/asset/${id}`]); } /** * Function responsible for navigating the user back to the main dashboard */ navigateToDashboard() { this.router.navigate([`dashboard`]); } /** * Function responsible for navigating the user to the assessment area to create * a new assessment */ navigateToCreateAsset() { this.router.navigate([`organization/${this.orgId}/asset-form`]); } /** * Function responsible for navigating the user to Asset Area * @param assetId asset ID passed required */ navigateToAsset(assetId: number) { this.router.navigate([`organization/${this.orgId}/asset-form/${assetId}`]); } /** * Function responsible for navigating the user to the assets open vulnereabilities * @param assetId asset ID passed required */ showOpenVulnsModal(assetId: number, assetName: string) { this.assetId = assetId; this.displayOpenVulnModal = true; this.assetNameHeader = assetName; this.openVulns = []; this.appService .getOpenVulnsByAssetId(this.assetId) .subscribe((openVulns) => { this.openVulns = openVulns; }); } /** * Function responsible for archiving an asset */ archiveAsset(asset: Asset) { const confirmed = confirm(`Archive the asset "${asset.name}"?`); if (confirmed) { this.appService.archiveAsset(asset).subscribe((res: string) => { this.alertService.success(res); this.getActiveAssets(); }); } } /** * Function responsible for activating an asset */ activateAsset(asset: Asset) { const confirmed = confirm(`Activate the asset "${asset.name}"?`); if (confirmed) { this.appService.activateAsset(asset).subscribe((res: string) => { this.alertService.success(res); this.getArchivedAssets(); }); } } onRiskChange(event) { const selectedRiskAry = event.value.map((x) => x.name); this.vulnTable.filter(selectedRiskAry, 'risk', 'in'); } navigateToVulnDetail(vulnId: number, assessmentId: number) { const url = this.router.serializeUrl( this.router.createUrlTree([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${assessmentId}/vuln-form/${vulnId}`, ]) ); let baseUrl = window.location.href.replace(this.router.url, ''); window.open(baseUrl + url, '_blank'); } } ================================================ FILE: frontend/src/app/page-not-found/page-not-found.component.html ================================================

Page not found

================================================ FILE: frontend/src/app/page-not-found/page-not-found.component.sass ================================================ ================================================ FILE: frontend/src/app/page-not-found/page-not-found.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { PageNotFoundComponent } from './page-not-found.component'; describe('PageNotFoundComponent', () => { let component: PageNotFoundComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ PageNotFoundComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(PageNotFoundComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/page-not-found/page-not-found.component.ts ================================================ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-page-not-found', templateUrl: './page-not-found.component.html', styleUrls: ['./page-not-found.component.sass'] }) export class PageNotFoundComponent implements OnInit { constructor() { } ngOnInit() { } } ================================================ FILE: frontend/src/app/password-reset/password-reset.component.html ================================================ ================================================ FILE: frontend/src/app/password-reset/password-reset.component.sass ================================================ ================================================ FILE: frontend/src/app/password-reset/password-reset.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { PasswordResetComponent } from './password-reset.component'; describe('PasswordResetComponent', () => { let component: PasswordResetComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ PasswordResetComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(PasswordResetComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/password-reset/password-reset.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; @Component({ selector: 'app-password-reset', templateUrl: './password-reset.component.html', styleUrls: ['./password-reset.component.sass'], }) export class PasswordResetComponent implements OnInit { uuid: string; pwdResetForm: FormGroup; constructor( private activatedRoute: ActivatedRoute, private fb: FormBuilder, public authService: AuthService, public router: Router, public alertService: AlertService ) { this.uuid = this.activatedRoute.snapshot.paramMap.get('uuid'); this.createForm(); } ngOnInit(): void {} createForm() { this.pwdResetForm = this.fb.group({ password: ['', Validators.required], confirmPassword: ['', Validators.required], }); } onSubmit(form) { const pwdUpdateObj = { password: form.value.password, confirmPassword: form.value.confirmPassword, uuid: this.uuid, }; this.authService.passwordReset(pwdUpdateObj).subscribe((res: string) => { this.alertService.success(res); this.router.navigate(['login']); }); } } ================================================ FILE: frontend/src/app/register/register.component.html ================================================

Bulwark Registration


================================================ FILE: frontend/src/app/register/register.component.sass ================================================ ================================================ FILE: frontend/src/app/register/register.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { RegisterComponent } from './register.component'; describe('RegisterComponent', () => { let component: RegisterComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ RegisterComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(RegisterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/register/register.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { UserService } from '../user.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.sass'], }) export class RegisterComponent implements OnInit { uuid: string; registerForm: FormGroup; constructor( private activatedRoute: ActivatedRoute, private fb: FormBuilder, public userService: UserService, public router: Router, public alertService: AlertService ) { this.uuid = this.activatedRoute.snapshot.paramMap.get('uuid'); this.createForm(); } ngOnInit(): void {} createForm() { this.registerForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], title: ['', Validators.required], password: ['', Validators.required], confirmPassword: ['', Validators.required], }); } onSubmit(form) { const registerObj = { firstName: form.value.firstName, lastName: form.value.lastName, title: form.value.title, password: form.value.password, confirmPassword: form.value.confirmPassword, uuid: this.uuid, }; this.userService.registerUser(registerObj).subscribe((res: string) => { this.router.navigate(['login']); this.alertService.success(res); }); } } ================================================ FILE: frontend/src/app/report/report.component.html ================================================

{{ report?.org?.name }}: {{ report?.asset?.name }} & {{ report?.assessment?.name }} Application Security Report


{{ report?.assessment?.startDate | date: 'shortDate':'UTC' }} - {{ report?.assessment?.endDate | date: 'shortDate':'UTC' }}

The {{ report?.companyName }} Application Security Team performed a security assessment on {{ report?.asset?.name }} & {{ report?.assessment?.name }} (the application) on {{ report?.assessment?.startDate | date: 'shortDate':'UTC' }}. The security assessment lasted {{ numOfDays }} {{ pluralDays }} and ended on {{ report?.assessment?.endDate | date: 'shortDate':'UTC' }}. This report contains detailed information about security vulnerabilities found during the assessment.


Executive Summary

{{ report?.assessment?.executiveSummary }}

Application Security Team

Name Position
{{ tester?.firstName }} {{tester?.lastName}} {{ tester?.title }}

Overall Risk Severity

Status


Summary of Findings

ID Finding Overall Risk Severity Status
{{ vuln?.id }} {{ vuln?.name }} {{ vuln?.risk }} {{ vuln?.status }}

No vulnerabilities were found during this assessment.


{{ vuln?.name }}

ID Impact Likelihood Risk Systemic CVSS Score Status
{{ vuln?.id }} {{ vuln?.impact }} {{ vuln?.likelihood }} {{ vuln?.risk }} {{ vuln?.systemic }} {{ vuln.cvssScore }} {{ vuln.status }}
Problem Location Target
{{ probLoc?.location }} {{ probLoc?.target }}

Description

{{ vuln?.description }}

Detailed Description

{{ vuln?.detailedInfo }}

Remediation

{{ vuln?.remediation }}

Resources

Description URL
{{ resource?.description }} {{ resource?.url }}

Screenshots

================================================ FILE: frontend/src/app/report/report.component.sass ================================================ div &.gallery margin: 5px border: 1px solid #ccc width: 80% &:hover border: 1px solid #777 img width: 100% height: auto &.disc padding: 15px text-align: center @media print div &.page break-before: page ================================================ FILE: frontend/src/app/report/report.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ReportComponent } from './report.component'; describe('ReportComponent', () => { let component: ReportComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ ReportComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ReportComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/report/report.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AppService } from '../app.service'; import { Vulnerability } from '../vuln-form/Vulnerability'; @Component({ selector: 'app-report', templateUrl: './report.component.html', styleUrls: ['./report.component.sass'], }) export class ReportComponent implements OnInit { report: any; numOfDays: number; pluralDays: string; orgId: number; assetId: number; assessmentId: number; isLoading = true; urls = []; showButtons = true; pieData: any; radarData: any; constructor( public activatedRoute: ActivatedRoute, public appService: AppService, public router: Router ) {} ngOnInit() { const pathAry = this.activatedRoute.snapshot.url.map((obj) => obj.path); if (pathAry.includes('puppeteer')) { this.showButtons = false; } this.activatedRoute.data.subscribe(({ report }) => { this.report = report; this.sortRisk(this.report.vulns); this.numOfDays = Math.floor( (Date.parse(this.report.assessment.endDate) - Date.parse(this.report.assessment.startDate)) / 86400000 ); this.pluralDays = this.numOfDays > 1 || this.numOfDays === 0 ? 'days' : 'day'; this.buildPieChart(report.vulns); this.buildRadarChart(report.vulns); for (const vuln of report.vulns) { vuln.screenshotObjs = []; for (const screenshot of vuln.screenshots) { this.appService.getImageById(screenshot).then((url) => { const screenshotObj = { url, name: screenshot.originalname, }; this.urls.push(url); vuln.screenshotObjs.push(screenshotObj); }); } } }); this.activatedRoute.params.subscribe((params) => { this.orgId = params.orgId; this.assetId = params.assetId; this.assessmentId = params.assessmentId; }); } sortRisk(vulns: Vulnerability[]) { // https://stackoverflow.com/a/14872766 const ordering = {}; const sortOrder = ['Critical', 'High', 'Medium', 'Low', 'Informational']; for (let i = 0; i < sortOrder.length; i++) { ordering[sortOrder[i]] = i; } vulns.sort((a, b) => { return ( ordering[a.risk] - ordering[b.risk] || a.name.localeCompare(b.risk) ); }); } buildPieChart(vulns: Vulnerability[]) { const infoVulns = vulns.filter((x) => x.risk === 'Informational').length; const lowVulns = vulns.filter((x) => x.risk === 'Low').length; const mediumVulns = vulns.filter((x) => x.risk === 'Medium').length; const highVulns = vulns.filter((x) => x.risk === 'High').length; const criticalVulns = vulns.filter((x) => x.risk === 'Critical').length; this.pieData = { labels: [ `Informational (${infoVulns})`, `Low (${lowVulns})`, `Medium (${mediumVulns})`, `High (${highVulns})`, `Critical (${criticalVulns})`, ], datasets: [ { data: [infoVulns, lowVulns, mediumVulns, highVulns, criticalVulns], backgroundColor: [ '#205493', '#2e8540', '#fdb81e', '#981b1e', '#e31c3d', ], hoverBackgroundColor: [ '#0071bc', '#4aa564', '#f9c642', '#c32225', '#e94964', ], }, ], }; } buildRadarChart(vulns: Vulnerability[]) { const open = vulns.filter((x) => x.status === 'Open').length; const resolved = vulns.filter((x) => x.status === 'Resolved').length; const onHold = vulns.filter((x) => x.status === 'On Hold').length; this.radarData = { labels: ['Open', 'Resolved', 'On Hold'], datasets: [ { label: 'Finding Status', backgroundColor: 'rgba(179,181,198,0.2)', borderColor: 'rgba(179,181,198,1)', pointBackgroundColor: 'rgba(179,181,198,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(179,181,198,1)', data: [open, resolved, onHold], }, ], }; } /** * Function responsible for navigating the user to the Vulnerability Listing */ navigateToVulns() { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${this.assessmentId}/vulnerability`, ]); } /** * Function that triggers when viewing vulnerabilities listing for an assessment * Data is passed from the report component object and handled within the function */ generateReport() { this.appService .generateReport(this.orgId, this.assetId, this.assessmentId) .subscribe((res: Blob) => { const blob = new Blob([res], { type: res.type, }); const url = window.URL.createObjectURL(blob); window.open(url); }); } } ================================================ FILE: frontend/src/app/team/team.component.html ================================================

Team Management


Name
Organization
Assets Role # of Members
{{team?.name}} {{team?.role !== 'Admin' ? team?.organization?.name : 'N/A'}} N/A {{asset.name}}{{isLast ? '' : ', '}} {{team?.role}} {{team?.users?.length}}

================================================ FILE: frontend/src/app/team/team.component.sass ================================================ ================================================ FILE: frontend/src/app/team/team.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TeamComponent } from './team.component'; describe('TeamComponent', () => { let component: TeamComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TeamComponent], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TeamComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/team/team.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { AlertService } from '../alert/alert.service'; import { Team } from '../interfaces/Team'; import { TeamService } from '../team.service'; import { UserService } from '../user.service'; import { Table } from 'primeng/table'; import { Router } from '@angular/router'; @Component({ selector: 'app-team', templateUrl: './team.component.html', styleUrls: ['./team.component.sass'], }) export class TeamComponent implements OnInit { teams: Team[]; constructor( public userService: UserService, public alertService: AlertService, public teamService: TeamService, public router: Router ) {} @ViewChild('teamTable') table: Table; ngOnInit(): void { this.getTeams(); } getTeams() { this.teamService.getTeams().subscribe((teams) => (this.teams = teams)); } navigateToTeamCreateForm() { this.router.navigate([`administration/team`]); } deleteTeam(team: Team) { const r = confirm(`Delete the team "${team.name}"`); if (r) { this.teamService.deleteTeam(team.id).subscribe((res: string) => { this.alertService.success(res); this.getTeams(); }); } } navigateToTeam(team: Team) { this.router.navigate([`administration/team/${team.id}`]); } } ================================================ FILE: frontend/src/app/team-form/team-form.component.html ================================================

× {{item.firstName}} {{item.lastName}} {{item.firstName}} {{item.lastName}}

The selected organization has no active assets


================================================ FILE: frontend/src/app/team-form/team-form.component.sass ================================================ ================================================ FILE: frontend/src/app/team-form/team-form.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TeamFormComponent } from './team-form.component'; describe('TeamFormComponent', () => { let component: TeamFormComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TeamFormComponent], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TeamFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/team-form/team-form.component.ts ================================================ import { Component, OnInit, OnChanges } from '@angular/core'; import { ROLE } from '../enums/roles.enum'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { TeamService } from '../team.service'; import { AlertService } from '../alert/alert.service'; import { Team } from '../interfaces/Team'; import { Organization } from '../org-form/Organization'; import { ActivatedRoute, Router } from '@angular/router'; import { AppService } from '../app.service'; import { Asset } from '../asset-form/Asset'; import { User } from '../interfaces/User'; interface Role { name: string; } @Component({ selector: 'app-team-form', templateUrl: './team-form.component.html', styleUrls: ['./team-form.component.sass'], }) export class TeamFormComponent implements OnInit, OnChanges { roles: Role[]; teamForm: FormGroup; organizations: Organization[]; assets: Asset[]; activeUsers: User[]; teamModel: Team; isCreate = true; teamId: number; constructor( private fb: FormBuilder, public alertService: AlertService, public teamService: TeamService, public appService: AppService, public activatedRoute: ActivatedRoute, public route: Router ) { this.createForm(); } ngOnInit(): void { this.activatedRoute.data.subscribe(({ result }) => { this.organizations = result.organizations; this.activeUsers = result.activeUsers; if (this.organizations && this.organizations.length) { this.appService .getOrganizationAssets(this.organizations[0].id) .then((assets: Asset[]) => { // tslint:disable-next-line: no-string-literal this.teamForm.controls['organization'].setValue( this.organizations[0] ); this.assets = assets; }); } else { this.alertService.warn( 'Tester and Read-Only teams require an organization and no organizations exist in the system.' ); } this.activatedRoute.params.subscribe((param) => { if (param && param.teamId) { this.isCreate = false; this.teamId = param.teamId; this.teamService.getTeamById(param.teamId).subscribe((team: Team) => { const role: Role = { name: team.role, }; team.role = role; this.teamForm.patchValue(team); if (team.organization) { this.appService .getOrganizationAssets(team.organization.id) .then((assets: Asset[]) => { this.assets = assets; }); } }); } }); }); this.roles = [ { name: ROLE.READONLY }, { name: ROLE.TESTER }, { name: ROLE.ADMIN }, ]; } ngOnChanges() { this.rebuildForm(); } createForm() { this.teamForm = this.fb.group({ name: ['', [Validators.required]], role: ['', [Validators.required]], users: [''], assets: [''], organization: [''], }); } rebuildForm() { this.teamForm.reset({ name: this.teamModel.name, role: this.teamModel.role, users: this.teamModel.users, assets: this.teamModel.assets, organization: this.teamModel.organization, }); } onSubmit(form) { if (form.value.users) { form.value.users = form.value.users.map((x) => x.id); } if (form.value.assets) { form.value.assets = form.value.assets.map((x) => x.id); } const team: Team = { name: form.value.name, organization: form.value.organization, role: form.value.role.name, users: form.value.users, assetIds: form.value.assets, }; if (this.isCreate) { this.teamService.createTeam(team).subscribe((res: string) => { this.teamForm.reset(); this.route.navigate([`administration`]); this.alertService.success(res); }); } else { team.id = this.teamId; this.teamService.updateTeam(team).subscribe((res: string) => { this.teamForm.reset(); this.route.navigate([`administration`]); this.alertService.success(res); }); } } navigateToAdministration() { this.route.navigate([`administration`]); } fetchOrgAssets(event) { const orgId: number = event.id; this.appService.getOrganizationAssets(orgId).then((assets: Asset[]) => { this.assets = assets; }); } } ================================================ FILE: frontend/src/app/team.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { TeamService } from './team.service'; describe('TeamService', () => { let service: TeamService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(TeamService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/team.service.ts ================================================ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../environments/environment'; import { Team } from './interfaces/Team'; @Injectable({ providedIn: 'root', }) export class TeamService { api = environment.apiUrl; constructor(private http: HttpClient) {} getTeams() { return this.http.get(`${this.api}/team`); } getTeamById(teamId: number) { return this.http.get(`${this.api}/team/${teamId}`); } createTeam(team: Team) { return this.http.post(`${this.api}/team`, team); } updateTeam(team: Team) { return this.http.patch(`${this.api}/team`, team); } deleteTeam(teamId: number) { return this.http.delete(`${this.api}/team/${teamId}`); } } ================================================ FILE: frontend/src/app/user-form/user-form.component.html ================================================

================================================ FILE: frontend/src/app/user-form/user-form.component.sass ================================================ ================================================ FILE: frontend/src/app/user-form/user-form.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserFormComponent } from './user-form.component'; describe('UserFormComponent', () => { let component: UserFormComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserFormComponent], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(UserFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/user-form/user-form.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FilterMatchMode } from 'primeng/api'; import { Table } from 'primeng/table'; import { User } from '../interfaces/User'; import { UserService } from '../user.service'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AlertService } from '../alert/alert.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.sass'], }) export class UserFormComponent implements OnInit { users: User[]; userForm: FormGroup; constructor( public userService: UserService, private fb: FormBuilder, public alertService: AlertService, public router: Router ) {} ngOnInit(): void { this.createForm(); } createForm() { this.userForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], firstName: ['', [Validators.required]], lastName: ['', [Validators.required]], title: ['', [Validators.required]], password: ['', [Validators.required]], confirmPassword: ['', [Validators.required]], }); } onSubmit(form) { const user: User = { firstName: form.value.firstName, lastName: form.value.lastName, title: form.value.title, password: form.value.password, confirmPassword: form.value.confirmPassword, email: form.value.email, }; this.userService.createUser(user).subscribe((res: string) => { this.alertService.success(res); this.router.navigate(['administration']); }); } } ================================================ FILE: frontend/src/app/user-management/user-management.component.html ================================================

User Management


Full Name
Title
Status Teams Actions
{{user?.firstName}} {{user?.lastName}}
{{user?.email}}
{{user?.title}} {{user?.active ? 'Active': 'Inactive'}} {{team.name}}{{isLast ? '' : ', '}}

================================================ FILE: frontend/src/app/user-management/user-management.component.sass ================================================ td.fitwidth width: 1px white-space: nowrap .hide display: none .name:hover + .hide display: block ================================================ FILE: frontend/src/app/user-management/user-management.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserManagementComponent } from './user-management.component'; describe('UserManagementComponent', () => { let component: UserManagementComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserManagementComponent], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(UserManagementComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/user-management/user-management.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { User } from '../interfaces/User'; import { UserService } from '../user.service'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AlertService } from '../alert/alert.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-user-management', templateUrl: './user-management.component.html', styleUrls: ['./user-management.component.sass'], }) export class UserManagementComponent implements OnInit { users: User[]; userForm: FormGroup; constructor( public userService: UserService, public alertService: AlertService, public router: Router ) {} ngOnInit(): void { this.getUsers(); } getUsers() { this.userService .getAllUsers() .subscribe((fetchedUsers) => (this.users = fetchedUsers)); } navigateToTeamCreateUser() { this.router.navigate(['administration/user/create']); } navigateToTeamInviteUser() { this.router.navigate(['administration/user/invite']); } /** * Activate the given user * @param user User to activate */ activateUser(user: User) { this.userService.activateUser(user.id).subscribe( (message) => { this.alertService.success(message as string); this.getUsers(); }, (error) => { this.alertService.error(error); } ); } /** * Deactivate the given user * @param user User to deactivate */ deactivateUser(user: User) { this.userService.deactivateUser(user.id).subscribe( (message) => { this.alertService.success(message as string); this.getUsers(); }, (error) => { this.alertService.error(error); } ); } } ================================================ FILE: frontend/src/app/user-profile/user-profile.component.html ================================================

Profile



User Email Configuration


Please check your email at {{user?.newEmail}} to verify the address. To revoke this request, please click this link

Security

Password Requirements: Must be at least 12 characters, at least one uppercase characters, at least one lowercase characters,at least one digit, and at least one symbol.

Active API Key

A generated API key can be used in place of traditional username/password authentication, allowing for all actions against Bulwark that the user is authorized for.

ID: {{userApiKeyInfo?.id}}
Created on {{userApiKeyInfo?.createdDate | date: 'longDate':'UTC'}}
Last used on {{userApiKeyInfo?.lastUpdatedDate | date: 'longDate':'UTC'}}

An active API key does not exist.


Teams


Name Role {{team.name}} {{team.role}}
================================================ FILE: frontend/src/app/user-profile/user-profile.component.sass ================================================ ================================================ FILE: frontend/src/app/user-profile/user-profile.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UserProfileComponent } from './user-profile.component'; import { ReactiveFormsModule, ValidationErrors } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from '../app-routing.module'; import { By } from '@angular/platform-browser'; import { UserService } from '../user.service'; const userService = { patchUser: () => {}, }; describe('UserProfileComponent', () => { let component: UserProfileComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [UserProfileComponent], imports: [ReactiveFormsModule, HttpClientModule, AppRoutingModule], providers: [{ provide: UserService, useValue: userService }], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(UserProfileComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('it should fail when form is empty', () => { expect(component.userForm.valid).toBeFalsy(); }); it('it should fail when First Name is not valid', () => { const firstName = component.userForm.controls.firstName; expect(firstName.valid).toBeFalsy(); }); it('it should pass if First Name has errors', () => { let errors: ValidationErrors; const firstName = component.userForm.controls.firstName; errors = firstName.errors || {}; expect(errors).toBeTruthy(); }); it('it should fail when Last Name is not valid', () => { const lastName = component.userForm.controls.lastName; expect(lastName.valid).toBeFalsy(); }); it('it should pass if Last Name has errors', () => { let errors: ValidationErrors; const lastName = component.userForm.controls.lastName; errors = lastName.errors || {}; expect(errors).toBeTruthy(); }); it('it should fail when Title is not valid', () => { const title = component.userForm.controls.title; expect(title.valid).toBeFalsy(); }); it('it should pass if First Name has errors', () => { let errors: ValidationErrors; const title = component.userForm.controls.title; errors = title.errors || {}; expect(errors).toBeTruthy(); }); it('it should pass if On Submit is executed once', () => { expect(component.isEdit).toBeFalsy(); expect(component.userForm.enabled).toBeFalsy(); const userForm = fixture.debugElement.query(By.css('#userForm')); userForm.triggerEventHandler('submit', null); expect(component.isEdit).toBeTruthy(); expect(component.userForm.enabled).toBeTruthy(); }); it('it should pass if On Submit is executed twice', () => { const mockedUserService = fixture.debugElement.injector.get(UserService); spyOn(mockedUserService, 'patchUser'); expect(component.isEdit).toBeFalsy(); expect(component.userForm.enabled).toBeFalsy(); const userForm = fixture.debugElement.query(By.css('#userForm')); userForm.triggerEventHandler('submit', null); expect(component.isEdit).toBeTruthy(); expect(component.userForm.enabled).toBeTruthy(); component.userForm.controls.firstName.setValue('Foo'); component.userForm.controls.lastName.setValue('Bar'); component.userForm.controls.title.setValue('Profressor'); expect(component.userForm.valid).toBeTruthy(); expect(mockedUserService.patchUser); }); }); ================================================ FILE: frontend/src/app/user-profile/user-profile.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../auth.service'; import { Router, ActivatedRoute } from '@angular/router'; import { AlertService } from '../alert/alert.service'; import { User } from '../interfaces/User'; import { UserService } from '../user.service'; import { ApiKey } from '../interfaces/ApiKey'; @Component({ selector: 'app-user-profile', templateUrl: './user-profile.component.html', styleUrls: ['./user-profile.component.sass'], }) export class UserProfileComponent implements OnInit { userForm: FormGroup; securityForm: FormGroup; emailForm: FormGroup; isEdit = false; isSecurityEdit = false; isEmailEdit = false; user: User; userApiKeyInfo: ApiKey; constructor( private fb: FormBuilder, public authService: AuthService, public router: Router, public alertService: AlertService, public activatedRoute: ActivatedRoute, public userService: UserService ) { this.createForms(); } ngOnInit(): void { this.activatedRoute.data.subscribe(({ user }) => { this.user = user; this.rebuildForm(); this.rebuildSecurityForm(); this.rebuildEmailForm(); }); this.getApiKey(); } createForms() { this.userForm = this.fb.group({ firstName: [{ value: '', disabled: !this.isEdit }, Validators.required], lastName: [{ value: '', disabled: !this.isEdit }, Validators.required], title: [{ value: '', disabled: !this.isEdit }, Validators.required], }); this.securityForm = this.fb.group({ oldPassword: [ { value: '', disabled: !this.isSecurityEdit }, Validators.required, ], newPassword: [ { value: '', disabled: !this.isSecurityEdit }, Validators.required, ], confirmNewPassword: [ { value: '', disabled: !this.isSecurityEdit }, Validators.required, ], }); this.emailForm = this.fb.group({ email: [ { value: '', disabled: !this.isEmailEdit }, [Validators.required, Validators.email], ], newEmail: [ { value: '', disabled: !this.isEmailEdit }, [Validators.required, Validators.email], ], }); } rebuildForm() { this.userForm.reset({ firstName: this.user.firstName, lastName: this.user.lastName, title: this.user.title, }); } rebuildSecurityForm() { this.securityForm.reset({ oldPassword: '', newPassword: '', confirmNewPassword: '', }); } rebuildEmailForm() { this.emailForm.reset({ email: this.user.email, newEmail: '', }); } onSubmit(form: FormGroup) { if (!this.isEdit) { this.isEdit = true; this.userForm.enable(); } else { const userInfo: User = { firstName: form.value.firstName, lastName: form.value.lastName, title: form.value.title, }; this.userService.patchUser(userInfo).subscribe((res: string) => { this.alertService.success(res); this.isEdit = false; this.userForm.disable(); }); } } onSecuritySubmit(form: FormGroup) { if (!this.isSecurityEdit) { this.isSecurityEdit = true; this.securityForm.enable(); } else { this.authService .updatePassword( form.value.oldPassword, form.value.newPassword, form.value.confirmNewPassword ) .subscribe((res: string) => { this.alertService.success(res); this.isSecurityEdit = false; this.securityForm.disable(); this.securityForm.reset(); }); } } onEmailFormSubmit(form: FormGroup) { if (!this.isEmailEdit) { this.isEmailEdit = true; this.emailForm.reset(); this.emailForm.enable(); } else { this.authService .updateUserEmail(form.value.email, form.value.newEmail) .subscribe((res: string) => { this.alertService.success(res); this.isEmailEdit = false; this.emailForm.disable(); this.rebuildEmailForm(); }); } } revokeUpdateEmailRequest() { this.user.newEmail = ''; this.authService.revokeUserEmail().subscribe((res: string) => { this.alertService.success(res); this.isEmailEdit = false; this.emailForm.disable(); this.rebuildEmailForm(); }); } generateApiKey() { const confirmMessage = this.userApiKeyInfo ? 'Generating a new API key will deactivate the current API key. Do you want to continue?' : 'Are you sure you want to generate a new API key?'; const r = confirm(confirmMessage); if (r) { this.authService.generateApiKey().subscribe((res) => { this.alertService.success('API key successfully generated'); this.getApiKey(); alert(res); }); } } getApiKey() { this.authService.getApiKeyInfo().subscribe((res: ApiKey) => { this.userApiKeyInfo = res; }); } deactivateApiKey() { const r = confirm('Are you sure you want to deactivate this API key?'); if (r) { this.authService .deactivateApiKey(this.userApiKeyInfo.id) .subscribe((res: string) => { this.alertService.success(res); this.getApiKey(); }); } } } ================================================ FILE: frontend/src/app/user.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { UserService } from './user.service'; describe('UserService', () => { let service: UserService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(UserService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/user.service.ts ================================================ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../environments/environment'; import { User } from './interfaces/User'; @Injectable({ providedIn: 'root', }) export class UserService { api = environment.apiUrl; constructor(private http: HttpClient) {} registerUser(creds) { return this.http.post(`${this.api}/user/register`, creds); } inviteUser(email) { return this.http.post(`${this.api}/user/invite`, email); } getUser() { return this.http.get(`${this.api}/user`); } getUsers() { return this.http.get(`${this.api}/users`); } getTesters(orgId: number) { return this.http.get(`${this.api}/testers/${orgId}`); } getAllUsers() { return this.http.get(`${this.api}/users/all`); } patchUser(user: User) { return this.http.patch(`${this.api}/user`, user); } createUser(user: User) { return this.http.post(`${this.api}/user`, user); } /** * Activate a user (admin) * @param id user ID */ activateUser(id: string | number) { return this.http.patch(`${this.api}/user/activate/${id}`, {}); } /** * Deactivate a user (admin) * @param id user ID */ deactivateUser(id: string | number) { return this.http.patch(`${this.api}/user/deactivate/${id}`, {}); } } ================================================ FILE: frontend/src/app/vuln-form/Vulnerability.ts ================================================ import { ProblemLocation } from '../classes/ProblemLocation'; import { Resource } from '../classes/Resource'; export class Vulnerability { constructor( public id: number, public impact: string, public likelihood: string, public risk: string, public systemic: string, public status: string, public description: string, public remediation: string, public name: string, public assessment: number, public jiraId: string, public cvssScore: number, public cvssUrl: string, public detailedInfo: string, public screenshots: FormData, public problemLocations: ProblemLocation[], public resources: Resource[] ) {} } ================================================ FILE: frontend/src/app/vuln-form/vuln-form.component.html ================================================
Vulnerability name is required
Vulnerability name is required
Jira ticket is required
Systemic value is required
Impact is not valid
Likelihood is not valid
Risk not valid
CVSS Score is not valid
CVSS URL is not valid

Problem Location Target
Description is not valid
Detailed information is not valid
Remediation information is not valid

Description Resource URL

================================================ FILE: frontend/src/app/vuln-form/vuln-form.component.sass ================================================ .previewBox border: 1px solid #ced4da padding: .375rem .75rem border-radius: .25rem ================================================ FILE: frontend/src/app/vuln-form/vuln-form.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { VulnFormComponent } from './vuln-form.component'; describe('VulnFormComponent', () => { let component: VulnFormComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ VulnFormComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(VulnFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/vuln-form/vuln-form.component.ts ================================================ import { Component, OnChanges, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { AppService } from '../app.service'; import { AlertService } from '../alert/alert.service'; import { Vulnerability } from './Vulnerability'; import { AppFile } from '../interfaces/App_File'; import { Screenshot } from '../interfaces/Screenshot'; @Component({ selector: 'app-vuln-form', templateUrl: './vuln-form.component.html', styleUrls: ['./vuln-form.component.sass'], }) export class VulnFormComponent implements OnChanges, OnInit { vulnModel: Vulnerability; vulnForm: FormGroup; submitted = false; alertType: string; alertMessage: string; orgId: string; assetId: string; assessmentId: string; vulnId: number; vulnFormData: FormData; jiraHost: string; tempScreenshots: Screenshot[] = []; screenshotsToDelete: number[] = []; previewDescription = false; previewDetailedDesc = false; previewRemediation = false; impactAssess = 0; likelihoodAssess = 0; riskAssess = 0; readOnly: boolean; constructor( private appService: AppService, public activatedRoute: ActivatedRoute, public router: Router, private fb: FormBuilder, private alertService: AlertService ) { this.createForm(); } /** * Specific to the vulnerability form, init is called to retreive all data associated with the vuln-form component * Attach subscriptions to likelihood and impact for dynamic risk attribute in the form */ ngOnInit() { this.vulnForm.get('impact').valueChanges.subscribe((value) => { if (this.impactAssess === 0) { if (value === 'High') { this.impactAssess += 3; } else if (value === 'Medium') { this.impactAssess += 2; } else { this.impactAssess += 1; } this.updateRisk(); } else { this.impactAssess = 0; if (value === 'High') { this.impactAssess += 3; } else if (value === 'Medium') { this.impactAssess += 2; } else { this.impactAssess += 1; } this.updateRisk(); } }); this.vulnForm.get('likelihood').valueChanges.subscribe((value) => { if (this.likelihoodAssess === 0) { if (value === 'High') { this.likelihoodAssess += 3; } else if (value === 'Medium') { this.likelihoodAssess += 2; } else { this.likelihoodAssess += 1; } this.updateRisk(); } else { this.likelihoodAssess = 0; if (value === 'High') { this.likelihoodAssess += 3; } else if (value === 'Medium') { this.likelihoodAssess += 2; } else { this.likelihoodAssess += 1; } this.updateRisk(); } }); this.activatedRoute.data.subscribe(({ vulnInfo }) => { this.readOnly = vulnInfo.readOnly; if (this.readOnly) { this.vulnForm.disable(); } if (vulnInfo && vulnInfo.jiraHost) { this.jiraHost = vulnInfo.jiraHost; } if (vulnInfo && vulnInfo.vulnerability) { this.vulnModel = vulnInfo.vulnerability; for (const probLoc of vulnInfo.vulnerability.problemLocations) { this.probLocArr.push( this.fb.group({ id: probLoc.id, location: probLoc.location, target: probLoc.target, }) ); } for (const resource of vulnInfo.vulnerability.resources) { this.resourceArr.push( this.fb.group({ id: resource.id, description: resource.description, url: resource.url, }) ); } for (const file of vulnInfo.vulnerability.screenshots) { const existFile: AppFile = file; this.appService.getImageById(existFile).then((url) => { existFile.imgUrl = url; this.previewScreenshot(null, existFile); }); } this.vulnForm.patchValue(vulnInfo.vulnerability); } }); this.activatedRoute.params.subscribe((params) => { this.orgId = params.orgId; this.assetId = params.assetId; this.assessmentId = params.assessmentId; this.vulnId = params.vulnId; }); } ngOnChanges() { this.rebuildForm(); } /** * Function responsible for the generation of the Vulnerability form * this is a requirement for reactive forms in Angular */ createForm() { this.vulnForm = this.fb.group({ impact: ['', [Validators.required]], likelihood: ['', [Validators.required]], risk: ['', [Validators.required]], systemic: ['', [Validators.required]], status: ['', Validators.required], description: ['', [Validators.required, Validators.maxLength(4000)]], remediation: ['', [Validators.required, Validators.maxLength(4000)]], name: ['', [Validators.required]], jiraId: ['', []], cvssScore: ['', Validators.required], cvssUrl: ['', Validators.required], detailedInfo: ['', [Validators.required, Validators.maxLength(4000)]], problemLocations: this.fb.array([]), resources: this.fb.array([]), }); } /** * Function is required to rebuild the form when requested it is required * for reactive forms in Angular */ rebuildForm() { this.vulnForm.reset({ impact: this.vulnModel.impact, likelihood: this.vulnModel.likelihood, risk: this.vulnModel.risk, systemic: this.vulnModel.systemic, status: this.vulnModel.status, description: this.vulnModel.description, remediation: this.vulnModel.remediation, name: this.vulnModel.name, jiraId: this.vulnModel.jiraId, cvssScore: this.vulnModel.cvssScore, cvssUrl: this.vulnModel.cvssUrl, detailedInfo: this.vulnModel.detailedInfo, problemLocations: this.vulnModel.problemLocations, resources: this.vulnModel.resources, }); } /** * Gets array with all stored problem locations for retrevial by the UI * @return problem location array data to be passed into the form for submission */ get probLocArr() { return this.vulnForm.get('problemLocations') as FormArray; } /** * Function responsible for initializing the form fields for location and target * needed for pulling data when a vulnerability is edited or a new vulnerability resource * is added by the user with the '+' icon, specific to Problem Location */ initProbLocRows(): FormGroup { return this.fb.group({ location: '', target: '', }); } /** * Function responsible for adding content into the ProbLocArr[] * Populates within the reactive form for submission later in the process */ addProbLoc() { this.probLocArr.push(this.initProbLocRows()); } /** * Function responsible for removing content from the ProbLocArr[] * Removes elements from the array by index value * @param index is the ID of the index to be removed from the array */ deleteProbLoc(index: number) { this.probLocArr.removeAt(index); } /** * Get array with all stored resources required within the Vulnerability form * later used in the form submission call * @return retuns the array of resource locations added into the vulnerability form */ get resourceArr() { return this.vulnForm.get('resources') as FormArray; } /** * Function responsible for initializing the form fields for description and url * needed for pulling data when a vulnerability is edited or a new vulnerability resource * is added by the user with the '+' icon, specific to Resources */ initResourceRows(): FormGroup { return this.fb.group({ description: '', url: '', }); } /** * Function responsible for adding form data to the Resource Array * utilizing the data within the reactive form */ addResource() { this.resourceArr.push(this.initResourceRows()); } /** * Function responsible for deletion of a resource from the form * @param index is the associated value of the array index value to be removed */ deleteResource(index: number) { this.resourceArr.removeAt(index); } /** * Function is responsible for processing a file array to be used in the form * iterates over all attached files to be processed */ handleFileInput(files: FileList) { for (let i = 0; i < files.length; i++) { const file = files.item(i); this.previewScreenshot(file, null); } } /** * Function responsible for retreiving an image and processing it back to the UI * renders it back to the browser using the createObjectUrl feature */ previewScreenshot(file: File, existFile: AppFile) { // Image from DB if (existFile) { const screenshot: Screenshot = { url: existFile.imgUrl, file: existFile, fileName: existFile.originalname, fileId: existFile.id, }; this.tempScreenshots.push(screenshot); } else { const screenshot: Screenshot = { url: this.appService.createObjectUrl(file), file, fileName: file.name, fileId: null, }; this.tempScreenshots.push(screenshot); } } /** * Function responsible for removal of screenshots from the Vulnerability Form * @param file the associated index of the file to be removed */ deleteScreenshot(screenshot: Screenshot) { const index = this.tempScreenshots.indexOf(screenshot); if (index > -1) { this.screenshotsToDelete.push(screenshot.fileId); this.tempScreenshots.splice(index, 1); } } /** * Function responsible for populating the screenshot array and removal * of screenshots performed during data entry * @param screenshots object data for screenshots to be processed * @param screenshotsToDelete object data for screenshots to be removed */ finalizeScreenshots( screenshots: Screenshot[], screenshotsToDelete: number[] ) { this.vulnFormData.delete('screenshots'); if (screenshots.length) { for (const screenshot of screenshots) { this.vulnFormData.append('screenshots', screenshot.file); } } if (screenshotsToDelete.length) { this.vulnFormData.append( 'screenshotsToDelete', JSON.stringify(screenshotsToDelete) ); } } /** * Function to navigate to Vulnerabilities, takes no input from the user */ navigateToVulnerabilities() { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${this.assessmentId}/vulnerability`, ]); } /** * Function responsible for handling the form submission objects and data * @param vulnForm form object holding data to be processed */ onSubmit(vulnForm: FormGroup) { this.vulnFormData = new FormData(); const newScreenshots = this.tempScreenshots.filter( (screenshot) => !screenshot.fileId ); this.finalizeScreenshots(newScreenshots, this.screenshotsToDelete); this.vulnModel = vulnForm.value; this.vulnFormData.append('impact', this.vulnModel.impact); this.vulnFormData.append('likelihood', this.vulnModel.likelihood); this.vulnFormData.append('risk', this.vulnModel.risk); this.vulnFormData.append('systemic', this.vulnModel.systemic); this.vulnFormData.append('status', this.vulnModel.status); this.vulnFormData.append('description', this.vulnModel.description); this.vulnFormData.append('remediation', this.vulnModel.remediation); this.vulnFormData.append('jiraId', this.vulnModel.jiraId); this.vulnFormData.append('cvssScore', this.vulnModel.cvssScore.toString()); this.vulnFormData.append('cvssUrl', this.vulnModel.cvssUrl); this.vulnFormData.append('detailedInfo', this.vulnModel.detailedInfo); this.vulnFormData.append('assessment', this.assessmentId); this.vulnFormData.append('name', this.vulnModel.name); this.vulnFormData.append( 'problemLocations', JSON.stringify(this.vulnModel.problemLocations) ); this.vulnFormData.append( 'resources', JSON.stringify(this.vulnModel.resources) ); this.createOrUpdateVuln(this.vulnFormData); } /** * Function responsible for handling the vulnerability form data * processes the data for either creation or updating a vulnerability * @param vuln form object holding all data to be processed */ createOrUpdateVuln(vuln: FormData) { if (this.vulnId) { this.appService .updateVulnerability(this.vulnId, vuln) .subscribe((res: string) => { this.navigateToVulnerabilities(); this.alertService.success(res); }); } else { this.appService.createVuln(vuln).subscribe((res: string) => { this.navigateToVulnerabilities(); this.alertService.success(res); }); } } /** * Function responsible for either showing a preview or hiding a preview * within the form for showing the data in markup or as regular text for * Description */ toggleDescPreview() { this.previewDescription = !this.previewDescription; } /** * Function responsible for either showing a preview or hiding a preview * within the form showing the data in markup or as regular text for * Detailed Information */ toggleDetailedDescPreview() { this.previewDetailedDesc = !this.previewDetailedDesc; } /** * Function responsible for either showing a preview or hiding a preview * within the form showing the data in markup or as regular text for * Remediation */ toggleRemediationPreview() { this.previewRemediation = !this.previewRemediation; } /** * Function responsible for updating the risk value on the vulnerability form * based on impact and likelihood value held within the vuln-form object */ updateRisk() { const value: number = this.impactAssess + this.likelihoodAssess; this.riskAssess = value; if (value === 6) { this.vulnForm.patchValue({ risk: 'Critical', }); } else if (value === 5) { this.vulnForm.patchValue({ risk: 'High', }); } else if (value === 4) { this.vulnForm.patchValue({ risk: 'Medium', }); } else if (value === 3) { this.vulnForm.patchValue({ risk: 'Low', }); } else { this.vulnForm.patchValue({ risk: 'Informational', }); } } exportToJira() { const r = confirm( `Export vulnerability "${this.vulnModel.name}" to Jira host: ${this.jiraHost}?` ); if (r) { if (this.vulnForm.dirty) { this.alertService.warn( 'Vulnerability form updates detected. Please save the vulnerability before exporting to JIRA.' ); } else { this.appService .exportVulnToJira(this.vulnId) .subscribe((res: string) => { this.navigateToVulnerabilities(); this.alertService.success(res); }); } } } } ================================================ FILE: frontend/src/app/vulnerability/vulnerability.component.html ================================================
Vulnerability ID Vulnerability Name Risk Systemic Jira URL Status Actions
{{option.name}}
{{option.name}}
{{ vuln?.id }} {{ vuln?.name }} {{ vuln?.risk }} {{ vuln?.systemic }} {{ vuln?.jiraId }} {{ vuln?.status }}
================================================ FILE: frontend/src/app/vulnerability/vulnerability.component.sass ================================================ ================================================ FILE: frontend/src/app/vulnerability/vulnerability.component.spec.ts ================================================ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { VulnerabilityComponent } from './vulnerability.component'; describe('VulnerabilityComponent', () => { let component: VulnerabilityComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ VulnerabilityComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(VulnerabilityComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/vulnerability/vulnerability.component.ts ================================================ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Vulnerability } from '../vuln-form/Vulnerability'; import { AppService } from '../app.service'; import { AlertService } from '../alert/alert.service'; import { Table } from 'primeng/table'; @Component({ selector: 'app-vulnerability', templateUrl: './vulnerability.component.html', styleUrls: ['./vulnerability.component.sass'], }) export class VulnerabilityComponent implements OnInit { vulnAry: any = []; assetId: number; assessmentId: number; orgId: number; readOnly: boolean; risks = [ { name: 'Informational' }, { name: 'Low' }, { name: 'Medium' }, { name: 'High' }, { name: 'Critical' }, ]; statuses = [{ name: 'Open' }, { name: 'Resolved' }, { name: 'On Hold' }]; @ViewChild('vulnTable') table: Table; constructor( public activatedRoute: ActivatedRoute, public router: Router, public appService: AppService, public alertService: AlertService ) {} ngOnInit() { this.activatedRoute.data.subscribe(({ vulnerabilities }) => { this.vulnAry = vulnerabilities.vulnerabilities; this.readOnly = vulnerabilities.readOnly; }); this.activatedRoute.params.subscribe((params) => { this.assetId = params.assetId; this.assessmentId = params.assessmentId; this.orgId = params.orgId; }); } /** * Function to navigate the user to the Vulnerability Form * Takes no arguments, passes the org id, asset id, assessment id, and loads the form */ navigateToVulnerabilityForm() { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${this.assessmentId}/vuln-form`, ]); } /** * Function responsible for navigating to a vulnerability * @param vulnId is the ID of the vulnerability requested */ navigateToVulnerabilityFormById(vulnId: number) { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${this.assessmentId}/vuln-form/${vulnId}`, ]); } /** * Function responsible for navigating to an assessment, takes no params directly */ navigateToAssessments() { this.router.navigate([`organization/${this.orgId}/asset/${this.assetId}`]); } /** * Function responsible for navigating to report area, takes no params directly */ navigateToReport() { this.router.navigate([ `organization/${this.orgId}/asset/${this.assetId}/assessment/${this.assessmentId}/report`, ]); } getVulnerabilities() { this.appService.getVulnerabilities(this.assessmentId).subscribe((vulns) => { this.vulnAry = []; this.vulnAry = vulns; }); } /** * Function responsible for deleting a vulnerability * @param vuln associated data for the vulnerability, cleans up associations * of the vulnerability and the assessment it is assigned to by ID */ deleteVuln(vuln: Vulnerability) { const r = confirm(`Delete the vulnerability "${vuln.name}"`); if (r === true) { this.appService.deleteVuln(vuln.id).subscribe((success: string) => { this.getVulnerabilities(); this.alertService.success(success); }); } } onRiskChange(event) { const selectedRiskAry = event.value.map((x) => x.name); this.table.filter(selectedRiskAry, 'risk', 'in'); } onStatusChange(event) { const selectedStatusAry = event.value.map((x) => x.name); this.table.filter(selectedStatusAry, 'status', 'in'); } } ================================================ FILE: frontend/src/assets/.gitkeep ================================================ ================================================ FILE: frontend/src/assets/theme/button.scss ================================================ .btn-success.p-inputtext { background-color: #28a745; border-color: #28a745; color: white; } .btn-success.p-inputtext:enabled:focus { box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5); border-color: #28a745; } ================================================ FILE: frontend/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: "http://localhost:4500/api", }; ================================================ FILE: frontend/src/environments/environment.ts ================================================ // This file can be replaced during build by using the `fileReplacements` array. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { production: false, apiUrl: '', }; /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. * * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. ================================================ FILE: frontend/src/index.html ================================================ Bulwark ================================================ FILE: frontend/src/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('@angular-devkit/build-angular/plugins/karma') ], client: { clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { dir: require('path').join(__dirname, '../coverage/frontend'), reports: ['html', 'lcovonly', 'text-summary'], fixWebpackSourcePaths: true }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: frontend/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err)); ================================================ FILE: frontend/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. /** * Web Animations `@angular/platform-browser/animations` * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). */ // import 'web-animations-js'; // Run `npm install --save web-animations-js`. /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags.ts'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: frontend/src/styles.scss ================================================ @import "./assets/theme/button.scss"; .btn-primary.p-button, .btn-primary { background-color: #ae0a0a; border-color: #ae0a0a; color: #fff !important; } .btn-secondary.p-button, .btn-secondary { color: #fff; background-color: #6c757d !important; border-color: #6c757d !important; } .btn-warning.p-button, .btn-warning { color: #fff; background-color: #ffc107 !important; border-color: #ffc107 !important; } .btn-primary.p-button:hover, .btn-primary:hover { color: #910a0a; background-color: #910a0a !important; border-color: #ae0a0a !important; } .btn-primary.p-button:active, .btn-primary { background-color: #910a0a !important; } .btn-primary.p-button:focus, .btn-primary:focus { box-shadow: 0 0 0 0.2rem #910a0a40 !important; } .btn-primary:disabled { color: #fff; background-color: #ae0a0a; border-color: #ae0a0a; } h3 { color: "grey"; } body { padding-bottom: 120px; } .footer { position: absolute; left: 0; bottom: 0; width: 100%; height: 114px; } .ng-valid[required], .ng-invalid:not(form) { border-left: 5px solid #ae0a0a; /* red */ } .card { margin-top: 10px; } ================================================ FILE: frontend/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: any; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); ================================================ FILE: frontend/src/tsconfig.app.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "types": [] }, "files": [ "main.ts", "polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: frontend/src/tsconfig.spec.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", "types": [ "jasmine", "node" ] }, "files": [ "test.ts", "polyfills.ts" ], "include": [ "**/*.spec.ts", "**/*.d.ts" ] } ================================================ FILE: frontend/src/tslint.json ================================================ { "extends": "../tslint.json", "rules": { "directive-selector": [ true, "attribute", "app", "camelCase" ], "component-selector": [ true, "element", "app", "kebab-case" ] } } ================================================ FILE: frontend/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "downlevelIteration": true, "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, "module": "commonjs", "moduleResolution": "node", "types": ["node"], "experimentalDecorators": true, "importHelpers": true, "target": "es6", "typeRoots": [ "node_modules/@types" ], "lib": [ "es6", "dom", "es2017" ], "paths": { "url": ["node_modules/@types/node"] } } } ================================================ FILE: frontend/tslint.json ================================================ { "extends": "tslint:recommended", "rulesDirectory": [ "codelyzer" ], "rules": { "align": { "options": [ "parameters", "statements" ] }, "array-type": false, "arrow-parens": false, "arrow-return-shorthand": true, "curly": true, "deprecation": { "severity": "warn" }, "eofline": true, "import-blacklist": [ true, "rxjs/Rx" ], "import-spacing": true, "indent": { "options": [ "spaces" ] }, "interface-name": false, "max-classes-per-file": false, "max-line-length": [ true, 140 ], "member-access": false, "member-ordering": [ true, { "order": [ "static-field", "instance-field", "static-method", "instance-method" ] } ], "no-consecutive-blank-lines": false, "no-console": [ true, "debug", "info", "time", "timeEnd", "trace" ], "no-empty": false, "no-inferrable-types": [ true, "ignore-params" ], "no-non-null-assertion": true, "no-redundant-jsdoc": true, "no-switch-case-fall-through": true, "no-var-requires": false, "object-literal-key-quotes": [ true, "as-needed" ], "object-literal-sort-keys": false, "ordered-imports": false, "quotemark": [ true, "single" ], "semicolon": { "options": [ "always" ] }, "space-before-function-paren": { "options": { "anonymous": "never", "asyncArrow": "always", "constructor": "never", "method": "never", "named": "never" } }, "trailing-comma": false, "no-output-on-prefix": true, "no-inputs-metadata-property": true, "no-outputs-metadata-property": true, "no-host-metadata-property": true, "no-input-rename": true, "no-output-rename": true, "typedef-whitespace": { "options": [ { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" }, { "call-signature": "onespace", "index-signature": "onespace", "parameter": "onespace", "property-declaration": "onespace", "variable-declaration": "onespace" } ] }, "use-lifecycle-interface": true, "use-pipe-transform-interface": true, "component-class-suffix": true, "directive-class-suffix": true , "variable-name": { "options": [ "ban-keywords", "check-format", "allow-pascal-case" ] }, "whitespace": { "options": [ "check-branch", "check-decl", "check-operator", "check-separator", "check-type", "check-typecast" ] } } } ================================================ FILE: jest.config.js ================================================ // For a detailed explanation regarding each configuration property, visit: // https://jestjs.io/docs/en/configuration.html module.exports = { // Automatically clear mock calls and instances between every test clearMocks: true, // The directory where Jest should output its coverage files coverageDirectory: 'coverage', roots: ['/src'], transform: { '^.+\\.ts?$': 'ts-jest' }, testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$', collectCoverage: true, collectCoverageFrom: ['src/**/*.ts'], moduleFileExtensions: ['ts', 'js', 'json', 'node'], modulePathIgnorePatterns: ['/src/services', '/src/database', '/src/init'], coveragePathIgnorePatterns: [ '/src/interfaces', '/src/classes', '/src/entity', '/src/enums' ], setupFiles: ['/.jest/setEnvVars.js'] }; ================================================ FILE: ormconfig.js ================================================ module.exports = { type: process.env.DB_TYPE, host: process.env.DB_URL, port: process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, entities: [__dirname + '/dist/entity/*.js'], migrations: ['dist/database/migration/*.js'], cli: { migrationsDir: 'src/database/migration' }, logging: true, synchronize: false }; ================================================ FILE: package.json ================================================ { "name": "bulwark", "version": "8.0.0", "description": "An organizational asset and vulnerability management tool", "main": "index.js", "scripts": { "test:node": "node_modules/.bin/jest --config=jest.config.js --collectCoverage", "test:front": "cd frontend && npm run-script test", "test": "npm run-script test:node && npm run-script test:front", "start": "node dist/app.js", "start:dev": "concurrently --kill-others \"npm run tsc:watch\" \"npm run ngServe\"", "ngServe": "cd frontend && npm run-script start:dev", "tsc:watch": "./node_modules/.bin/tsc-watch --onSuccess \"npm start\"", "build:prod": "cd frontend && npm run-script build:prod", "build:dev": "cd frontend && npm run-script build:dev", "preinstall": "cd frontend && npm install", "postinstall": "rimraf dist && tsc && npm run config && npm run build:prod", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "docker:check": "node dist/init/docker-run-exec.js", "migration:run": "typeorm migration:run -d src/data-source.ts", "migration:init": "typeorm migration:generate -d src/data-source.ts CreateDatabase && rimraf dist && tsc", "migration:generate": "typeorm migration:generate -d src/data-source.ts Refactor && rimraf dist && tsc", "migration:create": "typeorm migration:create -d src/data-source.ts newInit && rimraf dist && tsc", "migration:revert": "typeorm migration:revert -d src/data-source.ts", "tsc": "rimraf dist && tsc", "lint": "tslint --project . && cd frontend && npm run-script lint", "lint:fix": "tslint --fix --project . && cd frontend && npm run-script lint --fix=true", "release": "standard-version", "commit": "npx git-cz", "config": "node dist/init/setEnv.js" }, "keywords": [ "web security", "web application security", "webappsec", "owasp", "pentest", "pentesting", "security", "vulnerable", "vulnerability" ], "author": "Softrams https://www.softrams.com", "contributors": [ "Bill Jones", "Joshua Seidel", "Darrell Richards", "Alexandre Zanni", "Brett Mayen", "Boucham Amine", "Mark Muth" ], "repository": { "type": "git", "url": "https://github.com/softrams/bulwark.git" }, "license": "MIT", "devDependencies": { "@angular/cli": "^18.2.1", "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", "@babel/preset-typescript": "^7.24.7", "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", "@types/jest": "29.5.12", "@types/node": "^22.5.0", "babel-jest": "29.7.0", "cz-conventional-changelog": "^3.3.0", "highlight.js": ">=11.10.0", "husky": "^9.1.5", "jest": "29.7.0", "lint-staged": "^15.2.9", "mock-express-request": "^0.2.2", "mock-express-response": "^0.3.0", "prettier": "3.3.3", "rimraf": "^6.0.1", "sqlite3": "^5.1.7", "standard-version": "^9.5.0", "ts-jest": "29.2.5", "tsc-watch": "^6.2.0", "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", "typescript": "^5.5.4" }, "dependencies": { "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "class-validator": "^0.14.1", "concurrently": "^8.2.2", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "helmet": "^7.1.0", "jira-client": "^8.2.2", "jira2md": "^3.0.1", "jsonwebtoken": "^9.0.2", "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "node-fetch": "^2.6.7", "nodemailer": "^6.9.14", "password-validator": "^5.3.0", "prod": "^1.0.1", "puppeteer": "^23.2.0", "reflect-metadata": "^0.2.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typeorm": "^0.3.20", "uuid": "^10.0.0" }, "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "pre-commit": "lint-staged" } }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, "lint-staged": { "*": "prettier --write" } } ================================================ FILE: src/app.ts ================================================ import * as express from 'express'; import * as path from 'path'; import * as fs from 'fs'; import * as dotenv from 'dotenv'; import * as bodyParser from 'body-parser'; import { AppDataSource } from './data-source'; const authController = require('./routes/authentication.controller'); import * as userController from './routes/user.controller'; const fileUploadController = require('./routes/file-upload.controller'); import * as orgController from './routes/organization.controller'; import * as assetController from './routes/asset.controller'; import * as assessmentController from './routes/assessment.controller'; import * as vulnController from './routes/vulnerability.controller'; import * as jwtMiddleware from './middleware/jwt.middleware'; import { generateReport } from './utilities/puppeteer.utility'; import * as configController from './routes/config.controller'; import * as teamController from './routes/team.controller'; import * as apiKeyController from './routes/api-key.controller'; const helmet = require('helmet'); const cors = require('cors'); // Environment variables are loaded from .env file in data-source.ts const app = express(); app.use(cors()); app.use( express.static(path.join(__dirname, '../frontend/dist/frontend'), { etag: false, }) ); app.use(helmet()); app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self' blob:", 'stackpath.bootstrapcdn.com'], scriptSrc: [ "'self'", 'code.jquery.com', 'stackpath.bootstrapcdn.com', '${serverIpAddress}', ], styleSrc: ["'self'", 'stackpath.bootstrapcdn.com', "'unsafe-inline'"], }, }) ); app.use(bodyParser.json({ limit: '2mb' })); app.use(bodyParser.urlencoded({ extended: true })); const serverPort = process.env.PORT || 5000; const serverIpAddress = process.env.SERVER_ADDRESS || '127.0.0.1'; app.set('port', serverPort); app.set('serverIpAddress', serverIpAddress); // tslint:disable-next-line: no-console // Initialize TypeORM connection AppDataSource.initialize() .then(async () => { console.info(`Database connection successful`); await configController.initialInsert(); // Protected Global Routes app.post( '/api/user/email', jwtMiddleware.checkToken, userController.updateUserEmail ); app.post( '/api/user/email/revoke', jwtMiddleware.checkToken, userController.revokeEmailRequest ); app.patch('/api/user', jwtMiddleware.checkToken, userController.patch); app.post('/api/user', jwtMiddleware.checkToken, userController.create); app.get('/api/user', jwtMiddleware.checkToken, userController.getUser); app.get('/api/user', jwtMiddleware.checkToken, userController.getUser); app.get( '/api/users/all', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], userController.getAllUsers ); app.get('/api/users', jwtMiddleware.checkToken, userController.getUsers); app.get( '/api/testers/:orgId', jwtMiddleware.checkToken, userController.getTesters ); app.patch( '/api/user/password', jwtMiddleware.checkToken, userController.updateUserPassword ); app.post( '/api/refresh', jwtMiddleware.checkRefreshToken, authController.refreshSession ); app.get( '/api/file/:id', jwtMiddleware.checkToken, fileUploadController.getFileById ); app.get( '/api/organization', jwtMiddleware.checkToken, orgController.getActiveOrgs ); app.get( '/api/organization/archive', jwtMiddleware.checkToken, orgController.getArchivedOrgs ); app.get( '/api/organization/:id', jwtMiddleware.checkToken, orgController.getOrgById ); app.get( '/api/organization/asset/:id', jwtMiddleware.checkToken, assetController.getOrgAssets ); app.get( '/api/organization/:id/asset/archive', jwtMiddleware.checkToken, assetController.getArchivedOrgAssets ); app.get( '/api/organization/:id/asset/:assetId', jwtMiddleware.checkToken, assetController.getAssetById ); app.get( '/api/asset/:assetId/open/vulnerabilities', jwtMiddleware.checkToken, assetController.getOpenVulnsByAsset ); app.get( '/api/assessment/:id', jwtMiddleware.checkToken, assessmentController.getAssessmentsByAssetId ); app.get( '/api/assessment/:id/vulnerability', jwtMiddleware.checkToken, assessmentController.getAssessmentVulns ); app.post( '/api/assessment', jwtMiddleware.checkToken, assessmentController.createAssessment ); app.delete( '/api/assessment/:assessmentId', jwtMiddleware.checkToken, assessmentController.deleteAssessmentById ); app.get( '/api/asset/:assetId/assessment/:assessmentId', jwtMiddleware.checkToken, assessmentController.getAssessmentById ); app.patch( '/api/asset/:assetId/assessment/:assessmentId', jwtMiddleware.checkToken, assessmentController.updateAssessmentById ); app.get( '/api/assessment/:assessmentId/report', jwtMiddleware.checkToken, assessmentController.queryReportDataByAssessment ); app.post('/api/report/generate', jwtMiddleware.checkToken, generateReport); app.get( '/api/vulnerability/:vulnId', jwtMiddleware.checkToken, vulnController.getVulnById ); app.delete( '/api/vulnerability/:vulnId', jwtMiddleware.checkToken, vulnController.deleteVulnById ); app.patch( '/api/vulnerability/:vulnId', jwtMiddleware.checkToken, vulnController.patchVulnById ); app.post( '/api/vulnerability', jwtMiddleware.checkToken, vulnController.createVuln ); app.get( '/api/vulnerability/jira/:vulnId', jwtMiddleware.checkToken, vulnController.exportToJira ); app.get( '/api/user/teams', jwtMiddleware.checkToken, teamController.getMyTeams ); app.post( '/api/user/key', jwtMiddleware.checkToken, apiKeyController.generateApiKey ); app.get( '/api/user/key', jwtMiddleware.checkToken, apiKeyController.getUserApiKeyInfo ); app.patch( '/api/user/key/:id', jwtMiddleware.checkToken, apiKeyController.deleteApiKeyAsUser ); // Public Routes app.post('/api/user/register', userController.register); app.get('/api/user/verify/:uuid', userController.verify); app.patch('/api/forgot-password', authController.forgotPassword); app.patch('/api/password-reset', authController.resetPassword); app.post('/api/user/email/validate', userController.validateEmailRequest); app.post('/api/login', authController.login); // Tester Routes app.post( '/api/upload', jwtMiddleware.checkToken, fileUploadController.uploadFile ); // Admin Routes app.patch( '/api/organization/:id/asset/:assetId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], assetController.updateAssetById ); app.post( '/api/organization/:id/asset', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], assetController.createAsset ); app.patch( '/api/asset/archive/:assetId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], assetController.archiveAssetById ); app.patch( '/api/asset/activate/:assetId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], assetController.activateAssetById ); app.post( '/api/config', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], configController.saveConfig ); app.get( '/api/config', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], configController.getConfig ); app.post( '/api/user/invite', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], userController.invite ); // Activate user app.patch( '/api/user/activate/:id', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], userController.activateUser ); // Deactivate user app.patch( '/api/user/deactivate/:id', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], userController.deactivateUser ); app.patch( '/api/organization/:id/archive', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], orgController.archiveOrgById ); app.patch( '/api/organization/:id/activate', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], orgController.activateOrgById ); app.patch( '/api/organization/:id', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], orgController.updateOrgById ); app.post( '/api/organization', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], orgController.createOrg ); app.delete( '/api/asset/jira/:assetId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], assetController.purgeJiraInfo ); app.get( '/api/team/:teamId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.getTeamById ); app.get( '/api/team', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.getAllTeams ); app.post( '/api/team', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.createTeam ); app.patch( '/api/team', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.updateTeamInfo ); app.post( '/api/team/member/add', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.addTeamMember ); app.post( '/api/team/member/remove', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.removeTeamMember ); app.delete( '/api/team/:teamId', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.deleteTeam ); app.post( '/api/team/asset/add', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.addTeamAsset ); app.post( '/api/team/asset/remove', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], teamController.removeTeamAsset ); app.get( '/api/keys', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], apiKeyController.getAdminApiKeyInfo ); app.patch( '/api/key/:id', [jwtMiddleware.checkToken, jwtMiddleware.isAdmin], apiKeyController.deleteApiKeyAsAdmin ); // Start the server app.listen(serverPort, () => console.info(`Server running on ${serverIpAddress}:${serverPort}`) ); }) .catch((error) => { console.error('Error during Data Source initialization:', error); }); export { app }; ================================================ FILE: src/classes/Report.ts ================================================ import { Asset } from '../entity/Asset'; import { Organization } from '../entity/Organization'; import { Assessment } from '../entity/Assessment'; import { Vulnerability } from '../entity/Vulnerability'; export class Report { public org: Organization; public asset: Asset; public assessment: Assessment; public vulns: Vulnerability[]; public companyName: string; } ================================================ FILE: src/data-source.ts ================================================ import { DataSource } from 'typeorm'; import { User } from './entity/User'; import { Organization } from './entity/Organization'; import { Asset } from './entity/Asset'; import { Assessment } from './entity/Assessment'; import { Vulnerability } from './entity/Vulnerability'; import { File } from './entity/File'; import { ProblemLocation } from './entity/ProblemLocation'; import { Resource } from './entity/Resource'; import { Jira } from './entity/Jira'; import { ReportAudit } from './entity/ReportAudit'; import { ApiKey } from './entity/ApiKey'; import { Config } from './entity/Config'; import { Team } from './entity/Team'; import * as path from 'path'; import * as fs from 'fs'; import * as dotenv from 'dotenv'; // Load environment variables if (fs.existsSync(path.join(__dirname, '../.env'))) { const envPath = fs.readFileSync(path.join(__dirname, '../.env')); console.log('A .env file has been found and will now be parsed.'); const envConfig = dotenv.parse(envPath); if (envConfig) { for (const key in envConfig) { if (envConfig.hasOwnProperty(key)) { process.env[key] = envConfig[key]; } } console.log('The provided .env file has been parsed successfully.'); } } export const AppDataSource = new DataSource({ type: process.env.DB_TYPE as any || 'mysql', host: process.env.DB_URL || 'localhost', port: parseInt(process.env.DB_PORT || '3306'), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, synchronize: false, logging: process.env.NODE_ENV !== 'production', entities: [ User, Organization, Asset, Assessment, Vulnerability, File, ProblemLocation, Resource, Jira, ReportAudit, ApiKey, Config, Team ], migrations: [path.join(__dirname, './migration/*.js')], subscribers: [] }); ================================================ FILE: src/entity/ApiKey.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany, OneToOne, ManyToMany, } from 'typeorm'; import { IsDate, IsIn } from 'class-validator'; import { User } from './User'; @Entity() export class ApiKey { @PrimaryGeneratedColumn() id: number; @Column() key: string; @Column() secretKey: string; @Column() active: boolean; @Column() @IsDate() lastUpdatedBy: number; @Column() @IsDate() createdDate: Date; @Column() @IsDate() lastUpdatedDate: Date; @ManyToOne((type) => User, (user) => user.apiKey) user: User; } ================================================ FILE: src/entity/Assessment.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany, ManyToMany, JoinTable } from 'typeorm'; import { Asset } from './Asset'; import { Vulnerability } from './Vulnerability'; import { IsUrl, IsDate, MaxLength, IsString } from 'class-validator'; import { User } from './User'; @Entity() export class Assessment { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ length: 4000 }) executiveSummary: string; @Column() jiraId: string; @Column() @IsUrl() testUrl: string; @Column() @IsUrl() prodUrl: string; @Column() scope: string; @Column() @IsString() tag: string; @Column() @IsDate() startDate: Date; @Column() @IsDate() endDate: Date; @ManyToOne((type) => Asset, (asset) => asset.assessment, { onDelete: 'CASCADE' }) asset: Asset; @OneToMany((type) => Vulnerability, (vuln) => vuln.assessment) vulnerabilities: Vulnerability[]; @ManyToMany((type) => User) @JoinTable() testers: User[]; } ================================================ FILE: src/entity/Asset.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany, OneToOne, ManyToMany, } from 'typeorm'; import { Organization } from './Organization'; import { Assessment } from './Assessment'; import { IsIn } from 'class-validator'; import { Jira } from './Jira'; import { Team } from './Team'; @Entity() export class Asset { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() @IsIn(['A', 'AH']) status: string; @ManyToOne((type) => Organization, (organization) => organization.asset) organization: Organization; @OneToMany((type) => Assessment, (assessment) => assessment.asset) assessment: Assessment[]; @OneToOne((type) => Jira, (jira) => jira.asset) jira: Jira; @ManyToMany(() => Team, (team) => team.assets) teams: Team[]; } ================================================ FILE: src/entity/Config.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; import { IsEmail } from 'class-validator'; @Entity() export class Config { @PrimaryGeneratedColumn() id: number; @Column({ nullable: true }) @IsEmail() fromEmail: string; @Column({ nullable: true }) fromEmailPassword: string; @Column({ nullable: true }) companyName: string; } ================================================ FILE: src/entity/File.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { Vulnerability } from './Vulnerability'; import { DbAwareColumn } from '../utilities/column-mapper.utility'; @Entity() export class File { @PrimaryGeneratedColumn() id: number; @Column() fieldName: string; @Column() originalname: string; @Column() encoding: string; @Column() mimetype: string; @DbAwareColumn({ name: 'buffer', type: 'mediumblob' }) buffer: Buffer; @Column() size: number; @ManyToOne((type) => Vulnerability, (vuln) => vuln.screenshots, { onDelete: 'CASCADE' }) vulnerability: Vulnerability; } ================================================ FILE: src/entity/Jira.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'; import { IsUrl, IsNotEmpty } from 'class-validator'; import { Asset } from './Asset'; @Entity() export class Jira { @PrimaryGeneratedColumn() id: number; @Column() @IsUrl() @IsNotEmpty() host: string; @Column() @IsNotEmpty() apiKey: string; @Column() @IsNotEmpty() username: string; @OneToOne((type) => Asset, (asset) => asset.jira, { onDelete: 'CASCADE' }) @JoinColumn() asset: Asset; } ================================================ FILE: src/entity/Organization.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { Asset } from './Asset'; import { IsIn } from 'class-validator'; import { Team } from './Team'; @Entity() export class Organization { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() @IsIn(['A', 'AH']) status: string; @OneToMany((type) => Asset, (asset) => asset.organization) asset: Asset[]; @OneToMany((type) => Team, (team) => team.organization) teams: Team[]; } ================================================ FILE: src/entity/ProblemLocation.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { Vulnerability } from './Vulnerability'; @Entity() export class ProblemLocation { @PrimaryGeneratedColumn() id: number; @Column() location: string; @Column() target: string; @ManyToOne((type) => Vulnerability, (vuln) => vuln.problemLocations, { onDelete: 'CASCADE' }) vulnerability: Vulnerability; } ================================================ FILE: src/entity/ReportAudit.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { IsDate } from 'class-validator'; @Entity() export class ReportAudit { @PrimaryGeneratedColumn() id: number; @Column() assessmentId: number; @Column() generatedBy: number; @Column() @IsDate() generatedDate: Date; } ================================================ FILE: src/entity/Resource.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { Vulnerability } from './Vulnerability'; @Entity() export class Resource { @PrimaryGeneratedColumn() id: number; @Column() description: string; @Column() url: string; @ManyToOne((type) => Vulnerability, (vuln) => vuln.resources, { onDelete: 'CASCADE' }) vulnerability: Vulnerability; } ================================================ FILE: src/entity/Team.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable, ManyToOne, } from 'typeorm'; import { IsDate, IsIn } from 'class-validator'; import { User } from './User'; import { Organization } from './Organization'; import { Asset } from './Asset'; @Entity() export class Team { @PrimaryGeneratedColumn({}) id: number; @Column() name: string; @ManyToOne((type) => Organization, (organization) => organization.teams) organization: Organization; @Column() @IsDate() createdDate: Date; @Column({ nullable: true }) createdBy: number; @Column() @IsDate() lastUpdatedDate: Date; @Column({ nullable: true }) lastUpdatedBy: number; @Column() @IsIn(['Admin', 'Read-Only', 'Tester']) role: string; @ManyToMany(() => User, (user) => user.teams) @JoinTable() users: User[]; @ManyToMany(() => Asset, (asset) => asset.teams) @JoinTable() assets: Asset[]; } ================================================ FILE: src/entity/User.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, OneToMany, } from 'typeorm'; import { IsEmail, IsUUID, IsOptional } from 'class-validator'; import { dynamicNullable } from '../utilities/column-mapper.utility'; import { Team } from '../entity/Team'; import { ApiKey } from './ApiKey'; @Entity() export class User { @PrimaryGeneratedColumn({}) id: number; @Column({ unique: true, }) @IsEmail() email: string; @Column({ nullable: true, }) @IsEmail() newEmail: string; @dynamicNullable() password: string; @Column() active: boolean; @Column({ nullable: true, }) @IsOptional() @IsUUID() uuid: string; @dynamicNullable() firstName: string; @dynamicNullable() lastName: string; @dynamicNullable() title: string; @ManyToMany(() => Team, (team) => team.users) teams: Team[]; @OneToMany((type) => ApiKey, (apiKey) => apiKey.user) apiKey: ApiKey[]; } ================================================ FILE: src/entity/VulnDictionary.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class VulnDictionary { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() description: string; } ================================================ FILE: src/entity/Vulnerability.ts ================================================ import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, OneToMany } from 'typeorm'; import { Assessment } from './Assessment'; import { IsUrl, IsIn, MaxLength, IsAlpha, IsDecimal } from 'class-validator'; import { File } from './File'; import { ProblemLocation } from './ProblemLocation'; import { Resource } from './Resource'; @Entity() export class Vulnerability { @PrimaryGeneratedColumn() id: number; @Column() jiraId: string; @Column() @IsIn(['Low', 'Medium', 'High']) impact: string; @Column() @IsIn(['Low', 'Medium', 'High']) likelihood: string; @Column() @IsIn(['Low', 'Medium', 'High', 'Critical', 'Informational']) risk: string; @Column() @IsIn(['Yes', 'No']) systemic: string; @Column({ type: 'decimal', scale: 1, precision: 10 }) @IsDecimal() cvssScore: number; @Column() @IsUrl() cvssUrl: string; @Column() @IsIn(['Open', 'Resolved', 'On Hold']) status: string; @Column({ length: 4000 }) @MaxLength(4000) description: string; @Column({ length: 4000 }) @MaxLength(4000) detailedInfo: string; @Column({ length: 4000 }) @MaxLength(4000) remediation: string; @Column() name: string; @ManyToOne((type) => Assessment, (assessment) => assessment.vulnerabilities, { onDelete: 'CASCADE' }) assessment: Assessment; @OneToMany((type) => File, (file) => file.vulnerability) screenshots: File[]; @OneToMany((type) => ProblemLocation, (problemLocation) => problemLocation.vulnerability) problemLocations: ProblemLocation[]; @OneToMany((type) => Resource, (resource) => resource.vulnerability) resources: Resource[]; } ================================================ FILE: src/enums/message-enum.ts ================================================ export const passwordRequirement = 'Password Requirements: Must be at least 12 characters' + ', at least one uppercase characters, at least one lowercase characters,' + 'at least one digit, and at least one symbol.'; ================================================ FILE: src/enums/roles-enum.ts ================================================ export const ROLE = { ADMIN: 'Admin', READONLY: 'Read-Only', TESTER: 'Tester' }; ================================================ FILE: src/enums/status-enum.ts ================================================ export const status = { active: 'A', archived: 'AH' }; ================================================ FILE: src/init/docker-run-exec.ts ================================================ import { Connection, createConnection, getManager } from 'typeorm'; const exec = require('child_process').exec; let connection: Connection; export const initDbCheck = async () => { connection = await createConnection(); const manager = getManager(); let migrations; try { migrations = await manager.query('SELECT * FROM migrations'); } catch (err) { if (err) { console.error(err); } // tslint:disable-next-line: no-console console.info( 'Initial migration not found. Running generating initial migration.' ); // If the migrations table does not exist, run the migration:init script // to create schema exec('npm run migration:init', (err, stdout, stderr) => { if (err) { console.error(err); terminate(); } // tslint:disable-next-line: no-console console.log(stdout); exec('npm run migration:run', (runErr) => { if (runErr) { console.error(err); terminate(); } // tslint:disable-next-line: no-console console.info( 'Initial migration has been generated successfully. Running initial migration.' ); // tslint:disable-next-line: no-console console.log(stdout); terminate(); }); }); } if (migrations) { migrations = migrations.map((x) => x.name); // If the CreateDatabase migration exists skip migration:generate // else run the migration:generate script if (migrations[0].includes('CreateDatabase')) { // tslint:disable-next-line: no-console console.info( `Initial migration ${migrations[0]} exists. Skipping migration:init script` ); } // If the init migration was already created check to see // if there has been an update to the database // If there was a DB update, run the migration exec('npm run migration:generate', (err, stdout) => { if (err) { // tslint:disable-next-line: no-console console.info('No database updates detected'); terminate(); } console.log(stdout); // tslint:disable-next-line: no-console exec('npm run migration:run', (runErr, runStdout) => { // tslint:disable-next-line: no-console console.info('Database updates detected. Running generated migrations'); if (runErr) { console.error(runErr); terminate(); } // tslint:disable-next-line: no-console console.log('Database migration success!'); console.log(runStdout); terminate(); }); }); } }; const terminate = () => { connection.close(); process.exit(); }; initDbCheck(); ================================================ FILE: src/init/setEnv.ts ================================================ const dotenv = require('dotenv'); const fs = require('fs'); import * as path from 'path'; const { writeFile } = require('fs'); // Grabs .env variables if running locally if (fs.existsSync(path.join(__dirname, '../../.env'))) { const envPath = fs.readFileSync(path.join(__dirname, '../../.env')); // tslint:disable-next-line: no-console console.log('A .env file has been found found and will now be parsed.'); // https://github.com/motdotla/dotenv#what-happens-to-environment-variables-that-were-already-set const envConfig = dotenv.parse(envPath); if (envConfig) { for (const key in envConfig) { if (envConfig.hasOwnProperty(key)) { process.env[key] = envConfig[key]; } } // tslint:disable-next-line: no-console console.log('The provided .env file has been parsed successfully.'); } } // docker-compose will have access to the .env file if (process.env.SERVER_ADDRESS && process.env.PORT) { let targetPath: string; let isProduction: boolean; const apiUrl = `${process.env.SERVER_ADDRESS}:${process.env.PORT}/api`; if (process.env.NODE_ENV === 'production') { isProduction = true; targetPath = path.join( __dirname, '../../frontend/src/environments/environment.prod.ts' ); } else { isProduction = false; targetPath = path.join( __dirname, '../../frontend/src/environments/environment.dev.ts' ); } // we have access to our environment variables // in the process.env object thanks to dotenv const environmentFileContent = ` export const environment = { production: ${isProduction}, apiUrl: "${apiUrl}", }; `; // write the content to the respective file writeFile(targetPath, environmentFileContent, (err) => { if (err) { console.error(err); } // tslint:disable-next-line: no-console console.info( `Angular environment has been updated to ${environmentFileContent} located at ${targetPath}` ); return; }); } else { // tslint:disable-next-line: no-console console.info( 'Environment variables do not exist. Skipping `npm run config` step!' ); } ================================================ FILE: src/interfaces/jira/jira-init.interface.ts ================================================ export interface JiraInit { apiKey: string; host: string; username: string; } ================================================ FILE: src/interfaces/jira/jira-issue-link.interface.ts ================================================ export interface IssueLink { id?: number; outwardIssue?: { key: string; }; inwardIssue?: { key: string; }; type?: { name: string; }; } ================================================ FILE: src/interfaces/jira/jira-issue-priority.interface.ts ================================================ export interface JiraPriority { id?: number; key?: string; name?: string; colorName?: string; } ================================================ FILE: src/interfaces/jira/jira-issue-status.interface.ts ================================================ export interface IssueStatus { id: number; name: string; description: string; category: any; } ================================================ FILE: src/interfaces/jira/jira-issue-type.interface.ts ================================================ export interface IssueType { id?: string; name?: string; description?: string; properties?: any; } ================================================ FILE: src/interfaces/jira/jira-issue.interface.ts ================================================ import { JiraProject } from './jira-project.interface'; import { JiraPriority } from './jira-issue-priority.interface'; import { IssueType } from './jira-issue-type.interface'; import { IssueStatus } from './jira-issue-status.interface'; import { IssueLink } from './jira-issue-link.interface'; // Add interfaces as you need them export interface JiraIssue { update?: object; fields: { id?: number; key?: string; summary?: string; parent?: { key: string; }; subtasks?: JiraIssue[]; description?: any; environment?: string; project?: JiraProject; priority?: JiraPriority; assignee?: any; reporter?: any; creator?: any; issuetype?: IssueType; issueStatus?: IssueStatus; created?: Date; updated?: Date; dueDate?: Date; resolution?: any; originalEstimate?: number; remainingEstimate?: number; timeSpent?: number; securityLevel?: any; labels?: string[]; versions?: any[]; fixVersions?: any[]; components?: []; comments?: Comment[]; attachments?: []; links?: IssueLink[]; properties?: any; }; } ================================================ FILE: src/interfaces/jira/jira-project.interface.ts ================================================ export interface JiraProject { id?: string; key?: string; style?: string; name?: string; projectTypeKey?: string; properties?: any; } ================================================ FILE: src/interfaces/jira/jira-result.interface.ts ================================================ export interface JiraResult { id: string; key: string; self: string; message: string; } ================================================ FILE: src/interfaces/team-info.interface.ts ================================================ import { Organization } from '../entity/Organization'; export interface TeamInfo { id: number; role: string; organization: Organization; asset: number; } ================================================ FILE: src/interfaces/user-request.interface.ts ================================================ import { Request } from 'express'; import { Team } from '../entity/Team'; import { File } from '../entity/File'; export interface UserRequest extends Request { user: string; fileExtError: string; file: File; files: File[]; isAdmin: boolean; userTeams: Team[]; userOrgs: number[]; userAssets: number[]; } ================================================ FILE: src/middleware/jwt.middleware.ts ================================================ import jwt = require('jsonwebtoken'); import { Asset } from '../entity/Asset'; import { Organization } from '../entity/Organization'; import { UserRequest } from '../interfaces/user-request.interface'; import { AppDataSource } from '../data-source'; import { User } from '../entity/User'; import { ROLE } from '../enums/roles-enum'; import { Response } from 'express'; import { ApiKey } from '../entity/ApiKey'; import { compare } from '../utilities/password.utility'; /** * @description Checks for valid token before API logic * @param {Request} req * @param {Response} res */ export const checkToken = async (req: UserRequest, res: Response, next) => { if (req.headers.authorization) { const token = req.headers.authorization; // Express headers are auto converted to lowercase jwt.verify(token, process.env.JWT_KEY, async (err, decoded) => { if (err) { return res.status(401).json('Authorization token is not valid'); } else { req.user = decoded.userId; await fetchRoles(req); next(); } }); } else { const apiKey = req.header('Bulwark-Api-Key'); const secretApiKey = req.header('Bulwark-Secret-Key'); if (apiKey && secretApiKey) { await fetchRoles(req, res, apiKey, secretApiKey, next); } else { return res.status(401).json('Authorization token not supplied'); } } }; /** * @description Checks for valid token before API logic * @param {Request} req * @param {Response} res */ export const checkRefreshToken = (req, res, next) => { const token = req.body.refreshToken; if (token) { jwt.verify(token, process.env.JWT_REFRESH_KEY, (err, decoded) => { if (err) { return res.status(401).json('Authorization token is not valid'); } else { req.user = decoded.userId; next(); } }); } else { return res.status(401).json('Refresh token not supplied'); } }; /** * @description Validates Admin user role before executing route * @param {Request} req * @param {Response} res */ export const isAdmin = async (req, res, next) => { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: req.user }, relations: ['teams'] }); if (user && user.teams.some((team) => team.role === ROLE.ADMIN)) { next(); } else { return res.status(403).json('Authorization is required'); } }; // Determine if the user is an Administrator // If the user is an administrator, return all organizations and assets // Else return only the organization and assets associated to team export const fetchRoles = async ( req?: UserRequest, res?: Response, apiKey?: string | string[], secretKey?: string | string[], next? ) => { let user: User; if (apiKey && secretKey && res) { try { const apiKeyInfo = await fetchUserByApiKey(apiKey, secretKey); if (!apiKeyInfo) { return res .status(401) .json('The provided API key or Secret key is not valid'); } else { user = apiKeyInfo.user; req.user = user.id.toString(); await setUserRoles(req, user); next(); } } catch (error) { console.error('Error fetching API key:', error); return res.status(401).json('Authentication failed'); } } else { try { user = await fetchUserByJwt(req.user); await setUserRoles(req, user); } catch (error) { console.error('Error fetching user by JWT:', error); if (res) { return res.status(401).json('Authentication failed'); } } } }; const setUserRoles = async (req: UserRequest, user: User) => { const organizationRepository = AppDataSource.getRepository(Organization); const assetRepository = AppDataSource.getRepository(Asset); req.userTeams = user.teams; const isAdmin = req.userTeams.some((team) => team.role === ROLE.ADMIN); if (isAdmin) { const orgs = await organizationRepository.find(); const assets = await assetRepository.find(); req.userOrgs = orgs.map((org) => org.id); req.userAssets = assets.map((asset) => asset.id); req.isAdmin = true; } else { req.isAdmin = false; req.userOrgs = req.userTeams.map((team) => team.organization.id); req.userAssets = []; for (const team of req.userTeams) { if (team.assets && team.assets.length > 0) { const assets = team.assets.map((asset) => asset.id); req.userAssets.push(...assets); } } // Remove duplicate asset IDs req.userAssets = [...new Set(req.userAssets)]; } }; const fetchUserByApiKey = async ( apiKey: string | string[], secretKey: string | string[] ) => { const apiKeyRepository = AppDataSource.getRepository(ApiKey); const apiKeyInfo = await apiKeyRepository.findOne({ where: { key: apiKey.toString(), active: true }, relations: ['user', 'user.teams', 'user.teams.organization', 'user.teams.assets'] }); if (!apiKeyInfo) { return null; } const valid = await compare(secretKey, apiKeyInfo.secretKey); if (valid) { return apiKeyInfo; } else { return null; } }; const fetchUserByJwt = async (userId: string): Promise => { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: parseInt(userId) }, relations: ['teams', 'teams.organization', 'teams.assets'] }); if (!user) { throw new Error('User not found'); } return user; }; ================================================ FILE: src/middleware/jwt.spec.ts ================================================ require('dotenv').config({ path: '.env' }); import jwt = require('jsonwebtoken'); const { checkToken, checkRefreshToken } = require('./jwt.middleware'); describe('jwt.middleware.ts', () => { // Mocks the Request Object that is returned const mockRequest = () => { const req = { headers: { authorization: 'FakeJWT', }, body: {}, user: Function, }; req.user = jest.fn().mockReturnValue(req); return req; }; // Mocks the Response Object that is returned const mockResponse = () => { const res = { status: Function, json: Function, }; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); return res; }; let token: string; let refreshToken: string; beforeAll(() => { token = jwt.sign({ userId: '2222' }, process.env.JWT_KEY, { expiresIn: '15m', }); refreshToken = jwt.sign({ userId: '2222' }, process.env.JWT_REFRESH_KEY, { expiresIn: '8h', }); }); test('running checkToken should return a 401 with Authorization', async () => { const req = mockRequest(); const res = mockResponse(); await checkToken(req, res, null); expect(res.status).toHaveBeenCalledWith(401); }); test.skip('running checkToken should return a req.user', async () => { const req = mockRequest(); const res = mockResponse(); req.headers.authorization = token; await checkToken(req, res, jest.fn()); expect(req.user).toBe('2222'); }); test('running checkRefreshToken should return a 401 with refreshToken', async () => { const req = mockRequest(); const res = mockResponse(); req.body = { refreshToken: 'FakeJWT', }; await checkRefreshToken(req, res, null); expect(res.status).toHaveBeenCalledWith(401); }); test('running checkRefreshToken should return a 401 without refreshToken', async () => { const req = mockRequest(); const res = mockResponse(); await checkRefreshToken(req, res, null); expect(res.status).toHaveBeenCalledWith(401); }); test('running checkRefreshToken should return a req.user', async () => { const req = mockRequest(); const res = mockResponse(); req.body = { refreshToken, }; await checkRefreshToken(req, res, jest.fn()); expect(req.user).toBe('2222'); }); }); ================================================ FILE: src/routes/api-key.controller.spec.ts ================================================ import { createConnection, getConnection } from 'typeorm'; import * as configController from './config.controller'; import MockExpressResponse = require('mock-express-response'); import MockExpressRequest = require('mock-express-request'); import { User } from '../entity/User'; import { Team } from '../entity/Team'; import { compare } from '../utilities/password.utility'; import { v4 as uuidv4 } from 'uuid'; import { Assessment } from '../entity/Assessment'; import { Asset } from '../entity/Asset'; import { Jira } from '../entity/Jira'; import { Organization } from '../entity/Organization'; import { ProblemLocation } from '../entity/ProblemLocation'; import { ReportAudit } from '../entity/ReportAudit'; import { Resource } from '../entity/Resource'; import { Vulnerability } from '../entity/Vulnerability'; import { Config } from '../entity/Config'; import { File } from '../entity/File'; import { ApiKey } from '../entity/ApiKey'; import { deleteApiKeyAsAdmin, deleteApiKeyAsUser, generateApiKey, getAdminApiKeyInfo, getUserApiKeyInfo, } from './api-key.controller'; describe('config controller', () => { beforeEach(async () => { return createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Config, User, Team, Organization, Asset, Assessment, Vulnerability, ProblemLocation, ReportAudit, Resource, Jira, File, ApiKey, ], synchronize: true, logging: false, }); }); afterEach(() => { const conn = getConnection(); return conn.close(); }); test('Save API Key to User', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); }); test('Save Invalid API Key to User', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); }); test('Deactivate All Existing API Keys', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request2, response2); expect(response2.statusCode).toBe(200); }); test('Delete API Key as User', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); newUser = await getConnection() .getRepository(User) .findOne({ where: { id: newUser.id }, relations: ['apiKey'], }); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, params: { id: newUser.apiKey[0].id, }, }); await deleteApiKeyAsUser(request2, response2); expect(response2.statusCode).toBe(200); }); test('Delete Invalid API Key as User', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, params: { id: 1, }, }); await deleteApiKeyAsUser(request, response); expect(response.statusCode).toBe(404); }); test('Delete No API Key as User', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, params: {}, }); await deleteApiKeyAsUser(request, response); expect(response.statusCode).toBe(400); }); test('Delete API Key as Admin', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); newUser = await getConnection() .getRepository(User) .findOne({ where: { id: newUser.id }, relations: ['apiKey'], }); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, params: { id: newUser.apiKey[0].id, }, }); await deleteApiKeyAsAdmin(request2, response2); expect(response2.statusCode).toBe(200); }); test('Delete Invalid API Key as Admin', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, params: { id: 1, }, }); await deleteApiKeyAsAdmin(request, response); expect(response.statusCode).toBe(404); }); test('Delete No API Key as Admin', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, params: {}, }); await deleteApiKeyAsAdmin(request, response); expect(response.statusCode).toBe(400); }); test('Fetch User API Key Info', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); newUser = await getConnection() .getRepository(User) .findOne({ where: { id: newUser.id }, relations: ['apiKey'], }); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, }); await getUserApiKeyInfo(request2, response2); expect(response2.statusCode).toBe(200); expect(response2._getJSON().id).toBe(newUser.apiKey[0].id); }); test('Fetch User No API Key Info', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); newUser = await getConnection() .getRepository(User) .findOne({ where: { id: newUser.id }, relations: ['apiKey'], }); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, }); await getUserApiKeyInfo(request2, response2); expect(response2.statusCode).toBe(200); expect(response2._getJSON()).toBe(null); }); test('Fetch Admin API Key Info', async () => { let newUser = new User(); newUser.firstName = 'master'; newUser.lastName = 'chief'; newUser.email = 'testing@jest.com'; newUser.active = true; newUser = await getConnection().getRepository(User).save(newUser); const response = new MockExpressResponse(); const request = new MockExpressRequest({ user: newUser.id, }); await generateApiKey(request, response); expect(response.statusCode).toBe(200); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ user: newUser.id, }); await getAdminApiKeyInfo(request2, response2); expect(response2.statusCode).toBe(200); expect(response2._getJSON().length).toBe(1); }); }); ================================================ FILE: src/routes/api-key.controller.ts ================================================ import { User } from '../entity/User'; import { AppDataSource } from '../data-source'; import { UserRequest } from '../interfaces/user-request.interface'; import { ApiKey } from '../entity/ApiKey'; import * as crypto from 'crypto'; import { validate } from 'class-validator'; import { Response } from 'express'; import { generateHash } from '../utilities/password.utility'; /** * @description Generates an active API key. Deactivates deprecated keys * @param {UserRequest} req * @param {Response} res * @returns success message with API key */ export const generateApiKey = async (req: UserRequest, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const apiKeyRepository = AppDataSource.getRepository(ApiKey); const user = await userRepository.findOne({ where: { id: +req.user } }); if (!user) { return res.status(404).json('User not found'); } const buf = crypto.randomBytes(24); const secretBuf = crypto.randomBytes(24); const secretKey = await generateHash(secretBuf.toString('hex')); const apiKey: ApiKey = new ApiKey(); apiKey.key = buf.toString('hex'); apiKey.secretKey = secretKey; apiKey.createdDate = new Date(); apiKey.lastUpdatedDate = new Date(); apiKey.lastUpdatedBy = +req.user; apiKey.active = true; apiKey.user = user; await deactivateExistingApiKeys(user); const savedApiKey = await apiKeyRepository.save(apiKey); return res.status(200).json( `Write down the following keys and keep it in a safe place. You will not be able to retrieve the keys at a later time. Bulwark-Api-Key: ${savedApiKey.key} Bulwark-Secret-Key: ${secretBuf.toString('hex')}` ); } catch (error) { console.error('Error generating API key:', error); return res.status(500).json('An error occurred while generating the API key'); } }; /** * @description Deactivates all deprecated keys * @param {User} user * @returns n/a */ const deactivateExistingApiKeys = async (user: User) => { try { const apiKeyRepository = AppDataSource.getRepository(ApiKey); const activeApiKeys = await apiKeyRepository.find({ where: { user: { id: user.id }, active: true } }); if (activeApiKeys && activeApiKeys.length) { for (const activeApiKey of activeApiKeys) { activeApiKey.active = false; activeApiKey.lastUpdatedDate = new Date(); activeApiKey.lastUpdatedBy = user.id; await apiKeyRepository.save(activeApiKey); } } } catch (error) { console.error('Error deactivating existing API keys:', error); } }; /** * @description Soft deletes API key from user profile menu * @param {UserRequest} req * @param {Response} res * @returns success message */ export const deleteApiKeyAsUser = async (req: UserRequest, res: Response) => { try { const { id } = req.params; if (!id) { return res.status(400).json('Invalid API key'); } const apiKeyRepository = AppDataSource.getRepository(ApiKey); const apiKey = await apiKeyRepository .createQueryBuilder('apiKey') .leftJoinAndSelect('apiKey.user', 'user') .where('apiKey.id = :id', { id }) .andWhere('user.id = :userId', { userId: +req.user }) .getOne(); if (!apiKey) { return res.status(404).json('API key not found'); } apiKey.active = false; apiKey.lastUpdatedBy = +req.user; apiKey.lastUpdatedDate = new Date(); await apiKeyRepository.save(apiKey); return res.status(200).json('The API key has been deleted'); } catch (error) { console.error('Error deleting API key as user:', error); return res.status(500).json('An error occurred while deleting the API key'); } }; /** * @description Soft deletes API key from Admin menu * @param {UserRequest} req * @param {Response} res * @returns success message */ export const deleteApiKeyAsAdmin = async (req: UserRequest, res: Response) => { try { const { id } = req.params; if (!id) { return res.status(400).json('Invalid API key'); } const apiKeyRepository = AppDataSource.getRepository(ApiKey); const apiKey = await apiKeyRepository.findOne({ where: { id: parseInt(id) } }); if (!apiKey) { return res.status(404).json('API key not found'); } apiKey.active = false; apiKey.lastUpdatedDate = new Date(); apiKey.lastUpdatedBy = +req.user; await apiKeyRepository.save(apiKey); return res.status(200).json('The API key has been deactivated'); } catch (error) { console.error('Error deleting API key as admin:', error); return res.status(500).json('An error occurred while deleting the API key'); } }; /** * @description Retrieves Active API key metadata for user * @param {UserRequest} req * @param {Response} res * @returns API key information */ export const getUserApiKeyInfo = async (req: UserRequest, res: Response) => { try { const apiKeyRepository = AppDataSource.getRepository(ApiKey); const apiKeyInfo = await apiKeyRepository .createQueryBuilder('apiKey') .leftJoinAndSelect('apiKey.user', 'user') .where('apiKey.active = true') .andWhere('user.id = :userId', { userId: +req.user }) .select(['apiKey.createdDate', 'apiKey.lastUpdatedDate', 'apiKey.id']) .getOne(); if (!apiKeyInfo) { return res.status(200).json(null); } return res.status(200).json(apiKeyInfo); } catch (error) { console.error('Error getting user API key info:', error); return res.status(500).json('An error occurred while retrieving API key information'); } }; /** * @description Retrieves API key metadata for admin * @param {UserRequest} req * @param {Response} res * @returns API key information */ export const getAdminApiKeyInfo = async (req: UserRequest, res: Response) => { try { const apiKeyRepository = AppDataSource.getRepository(ApiKey); const apiKeyInfo = await apiKeyRepository .createQueryBuilder('apiKey') .leftJoinAndSelect('apiKey.user', 'user') .where('apiKey.active = true') .select([ 'apiKey.id', 'apiKey.createdDate', 'apiKey.lastUpdatedDate', 'user.email', ]) .getMany(); return res.status(200).json(apiKeyInfo); } catch (error) { console.error('Error getting admin API key info:', error); return res.status(500).json('An error occurred while retrieving API key information'); } }; ================================================ FILE: src/routes/assessment.controller.spec.ts ================================================ import { getConnection } from 'typeorm'; import { Assessment } from '../entity/Assessment'; import { Asset } from '../entity/Asset'; import { Vulnerability } from '../entity/Vulnerability'; import { Organization } from '../entity/Organization'; import { createConnection } from 'typeorm'; import { File } from '../entity/File'; import { User } from '../entity/User'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import MockExpressResponse = require('mock-express-response'); import MockExpressRequest = require('mock-express-request'); import * as assessmentController from './assessment.controller'; import { Jira } from '../entity/Jira'; import { Team } from '../entity/Team'; import { ROLE } from '../enums/roles-enum'; import { ApiKey } from '../entity/ApiKey'; describe('Assessment Controller', () => { beforeEach(async () => { await createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Asset, Organization, File, Vulnerability, Assessment, User, ProblemLocation, Resource, Jira, Team, ApiKey, ], synchronize: true, logging: false, name: 'default', }); }); afterEach(() => { const conn = getConnection('default'); return conn.close(); }); test('Assessment Delete', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(org); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'A', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset = await getConnection() .getRepository(Asset) .save(insertAsset); let existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; existUser = await getConnection().getRepository(User).save(existUser); const vulns: Vulnerability[] = []; const team: Team = { id: null, name: 'test team', organization: savedOrg, lastUpdatedBy: existUser.id, createdBy: existUser.id, lastUpdatedDate: new Date(), createdDate: new Date(), role: ROLE.TESTER, assets: [savedAsset], users: [existUser], }; const savedTeam = await getConnection().getRepository(Team).save(team); const assessment: Assessment = { id: null, name: 'Test Assessment', executiveSummary: 'Lol this is a bad report. Do not read', jiraId: 'not a jira id', testUrl: 'not a url', prodUrl: 'not a url', scope: 'everything', tag: '1.0.0', startDate: new Date(), endDate: new Date(), asset: savedAsset, testers: [existUser], vulnerabilities: vulns, }; const savedAssessment = await getConnection() .getRepository(Assessment) .save(assessment); const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { assessmentId: 'abc', }, userTeams: [savedTeam], }); await assessmentController.deleteAssessmentById(request, response); expect(response.statusCode).toBe(400); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ params: { assessmentId: null, }, userTeams: [savedTeam], }); await assessmentController.deleteAssessmentById(request2, response2); expect(response2.statusCode).toBe(400); const response3 = new MockExpressResponse(); const request3 = new MockExpressRequest({ params: { assessmentId: 3, }, userTeams: [savedTeam], }); await assessmentController.deleteAssessmentById(request3, response3); expect(response3.statusCode).toBe(404); const response4 = new MockExpressResponse(); const request4 = new MockExpressRequest({ params: { assessmentId: 1, }, userTeams: [savedTeam], }); await assessmentController.deleteAssessmentById(request4, response4); expect(response4.statusCode).toBe(200); }); test('get assessments by asset id', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(org); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'A', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset = await getConnection() .getRepository(Asset) .save(insertAsset); let existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; existUser = await getConnection().getRepository(User).save(existUser); const vulns: Vulnerability[] = []; const team: Team = { id: null, name: 'test team', organization: savedOrg, lastUpdatedBy: existUser.id, createdBy: existUser.id, lastUpdatedDate: new Date(), createdDate: new Date(), role: ROLE.TESTER, assets: [savedAsset], users: [existUser], }; const savedTeam = await getConnection().getRepository(Team).save(team); const assessment: Assessment = { id: null, name: 'Test Assessment', executiveSummary: 'Lol this is a bad report. Do not read', jiraId: 'not a jira id', testUrl: 'not a url', prodUrl: 'not a url', scope: 'everything', tag: '1.0.0', startDate: new Date(), endDate: new Date(), asset: savedAsset, testers: [existUser], vulnerabilities: vulns, }; const savedAssessment = await getConnection() .getRepository(Assessment) .save(assessment); const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { id: savedAsset.id, }, userAssets: [1], userTeams: [savedTeam], }); await assessmentController.getAssessmentsByAssetId(request, response); expect(response.statusCode).toBe(200); expect(response._getJSON().assessments.length).toBe(1); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ params: { blah: 1, }, }); await assessmentController.getAssessmentsByAssetId(request2, response2); expect(response2.statusCode).toBe(400); const response3 = new MockExpressResponse(); const request3 = new MockExpressRequest({ params: { id: 'abc', }, }); await assessmentController.getAssessmentsByAssetId(request3, response3); expect(response3.statusCode).toBe(400); const response4 = new MockExpressResponse(); const request4 = new MockExpressRequest({ params: { id: savedAsset.id, }, userAssets: [2], userTeams: [savedTeam], }); await assessmentController.getAssessmentsByAssetId(request4, response4); expect(response4.statusCode).toBe(404); }); }); ================================================ FILE: src/routes/assessment.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { Response } from 'express'; import { AppDataSource } from '../data-source'; import { Assessment } from '../entity/Assessment'; import { Asset } from '../entity/Asset'; import { validate } from 'class-validator'; import { Vulnerability } from '../entity/Vulnerability'; import { Organization } from '../entity/Organization'; import { Report } from '../classes/Report'; import { Config } from '../entity/Config'; import { hasAssetReadAccess, hasAssetWriteAccess, } from '../utilities/role.utility'; const userController = require('../routes/user.controller'); /** * @description Get assessments by asset ID * @param {UserRequest} req * @param {Response} res * @returns Asset assessments */ export const getAssessmentsByAssetId = async ( req: UserRequest, res: Response ) => { try { if (!req.params.id) { return res.status(400).json('Invalid Asset request'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Asset ID'); } const assetRepository = AppDataSource.getRepository(Asset); const assessmentRepository = AppDataSource.getRepository(Assessment); const asset = await assetRepository.findOne({ where: { id: +req.params.id }, relations: ['organization'] }); if (!asset) { return res.status(404).json('Asset does not exist'); } const assetAccess = await hasAssetReadAccess(req, asset.id); if (!assetAccess) { return res.status(404).json('Asset not found'); } const hasTesterAccess = await hasAssetWriteAccess(req, asset.id); const assessments = await assessmentRepository .createQueryBuilder('assessment') .leftJoinAndSelect('assessment.testers', 'tester') .where('assessment.assetId = :assetId', { assetId: asset.id, }) .select([ 'assessment.id', 'assessment.name', 'assessment.jiraId', 'assessment.startDate', 'assessment.endDate', 'tester.firstName', 'tester.lastName', ]) .getMany(); return res.status(200).json({ assessments, readOnly: !hasTesterAccess }); } catch (error) { console.error('Error fetching assessments:', error); return res.status(500).json('An error occurred while fetching assessments'); } }; /** * @description Get all vulnerabilities by assessment * @param {UserRequest} req * @param {Response} res * @returns assessment vulnerabilities */ export const getAssessmentVulns = async (req: UserRequest, res: Response) => { try { if (!req.params.id) { return res.status(400).json('Invalid Assessment ID'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const assessment = await assessmentRepository .createQueryBuilder('assessment') .leftJoinAndSelect('assessment.asset', 'asset') .leftJoinAndSelect('asset.organization', 'organization') .where('assessment.id = :assessmentId', { assessmentId: +req.params.id }) .select(['assessment', 'asset', 'organization']) .getOne(); if (!assessment) { return res.status(404).json('Assessment does not exist'); } const hasReadAccess = await hasAssetReadAccess(req, assessment.asset.id); if (!hasReadAccess) { return res.status(404).json('Assessment not found'); } const hasTesterAccess = await hasAssetWriteAccess(req, assessment.asset.id); const vulnerabilities = await vulnerabilityRepository.find({ where: { assessment: { id: +req.params.id } } }); if (!vulnerabilities) { return res.status(404).json('Vulnerabilities do not exist'); } return res.status(200).json({ vulnerabilities, readOnly: !hasTesterAccess }); } catch (error) { console.error('Error fetching vulnerabilities:', error); return res.status(500).json('An error occurred while fetching vulnerabilities'); } }; /** * @description Create assessment * @param {UserRequest} req * @param {Response} res c * @returns success message */ export const createAssessment = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.body.asset)) { return res.status(400).json('Asset ID is invalid'); } const assetRepository = AppDataSource.getRepository(Asset); const assessmentRepository = AppDataSource.getRepository(Assessment); const asset = await assetRepository.findOne({ where: { id: +req.body.asset }, relations: ['organization'] }); if (!asset) { return res.status(404).json('Asset does not exist'); } const hasAccess = await hasAssetWriteAccess(req, asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } if (!req.body.testers || !req.body.testers.length) { return res.status(400).json('No testers have been selected'); } const testers = await userController.getUsersById(req.body.testers); const assessment = new Assessment(); assessment.asset = asset; assessment.name = req.body.name; assessment.executiveSummary = req.body.executiveSummary; assessment.jiraId = req.body.jiraId; assessment.testUrl = req.body.testUrl; assessment.prodUrl = req.body.prodUrl; assessment.scope = req.body.scope; assessment.tag = req.body.tag; assessment.startDate = new Date(req.body.startDate); assessment.endDate = new Date(req.body.endDate); assessment.testers = testers; const errors = await validate(assessment); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Assessment form validation failed'); } await assessmentRepository.save(assessment); return res.status(200).json('Assessment created successfully'); } catch (error) { console.error('Error creating assessment:', error); return res.status(500).json('An error occurred while creating the assessment'); } }; /** * @description Get asset assessment by ID * @param {UserRequest} req * @param {Response} res * @returns assessment */ export const getAssessmentById = async (req: UserRequest, res: Response) => { try { if (!req.params.assessmentId) { return res.status(400).send('Invalid assessment request'); } if (isNaN(+req.params.assessmentId)) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const assessment = await assessmentRepository .createQueryBuilder('assessment') .leftJoinAndSelect('assessment.asset', 'asset') .leftJoinAndSelect('asset.organization', 'organization') .leftJoinAndSelect('assessment.testers', 'tester') .where('assessment.id = :assessmentId', { assessmentId: +req.params.assessmentId, }) .select([ 'assessment', 'asset', 'organization', 'tester.firstName', 'tester.lastName', 'tester.title', 'tester.id', ]) .getOne(); if (!assessment) { return res.status(404).json('Assessment does not exist'); } const hasReadAccess = await hasAssetReadAccess(req, assessment.asset.id); if (!hasReadAccess) { return res.status(404).json('Assessment not found'); } const hasTesterAccess = await hasAssetWriteAccess(req, assessment.asset.id); return res.status(200).json({ assessment, readOnly: !hasTesterAccess }); } catch (error) { console.error('Error fetching assessment:', error); return res.status(500).json('An error occurred while fetching the assessment'); } }; /** * @description Update assessment * @param {UserRequest} req * @param {Response} res * @returns success message */ export const updateAssessmentById = async (req: UserRequest, res: Response) => { try { if (!req.params.assessmentId) { return res.status(400).send('Invalid assessment request'); } if (isNaN(+req.params.assessmentId)) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); let assessment = await assessmentRepository.findOne({ where: { id: +req.params.assessmentId }, relations: ['testers', 'asset'] }); if (!assessment) { return res.status(404).json('Assessment does not exist'); } const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } if (!req.body.testers || !req.body.testers.length) { return res.status(400).json('No testers have been selected'); } const assessmentId = assessment.id; const assetId = assessment.asset.id; // Create a new assessment object from the request body const updatedAssessment = { ...req.body, id: assessmentId, asset: { id: assetId }, testers: await userController.getUsersById(req.body.testers) }; // Skip asset update since we're keeping the original asset delete updatedAssessment.asset; // Validate dates if (new Date(updatedAssessment.startDate) > new Date(updatedAssessment.endDate)) { return res .status(400) .send('The assessment start date cannot be later than the end date'); } // Create the updated assessment with all fields Object.assign(assessment, updatedAssessment); const errors = await validate(assessment); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Assessment form validation failed'); } await assessmentRepository.save(assessment); return res.status(200).json('Assessment updated successfully'); } catch (error) { console.error('Error updating assessment:', error); return res.status(500).json('An error occurred while updating the assessment'); } }; /** * @description Query information for assessment report * @param {UserRequest} req * @param {Response} res * @returns report information */ export const queryReportDataByAssessment = async ( req: UserRequest, res: Response ) => { try { if (!req.params.assessmentId) { return res.status(400).send('Invalid report request'); } if (isNaN(+req.params.assessmentId)) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const assetRepository = AppDataSource.getRepository(Asset); const organizationRepository = AppDataSource.getRepository(Organization); const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const configRepository = AppDataSource.getRepository(Config); // Get assessment with testers const assessment = await assessmentRepository .createQueryBuilder('assessment') .leftJoinAndSelect('assessment.testers', 'tester') .where('assessment.id = :assessmentId', { assessmentId: +req.params.assessmentId, }) .leftJoinAndSelect('assessment.asset', 'asset') .select([ 'assessment', 'asset.id', 'tester.firstName', 'tester.lastName', 'tester.title', ]) .getOne(); if (!assessment) { return res.status(404).json('Assessment not found'); } // Get assessment for asset const assessmentForId = await assessmentRepository.findOne({ where: { id: +req.params.assessmentId }, relations: ['asset'] }); const asset = await assetRepository.findOne({ where: { id: assessmentForId.asset.id }, relations: ['organization'] }); const hasReadAccess = await hasAssetReadAccess(req, asset.id); if (!hasReadAccess) { return res.status(404).json('Assessment not found'); } const organization = await organizationRepository.findOne({ where: { id: asset.organization.id } }); // Get vulnerabilities with relations const vulnerabilities = await vulnerabilityRepository .createQueryBuilder('vuln') .leftJoinAndSelect('vuln.screenshots', 'screenshot') .leftJoinAndSelect('vuln.problemLocations', 'problemLocation') .leftJoinAndSelect('vuln.resources', 'resource') .where('vuln.assessmentId = :assessmentId', { assessmentId: assessment.id, }) .select([ 'vuln', 'screenshot.id', 'screenshot.originalname', 'screenshot.mimetype', 'problemLocation', 'resource', ]) .getMany(); // Get company name from config const config = await configRepository.findOne({ where: { id: 1 }, select: ['companyName'] }); // Create report object const report = new Report(); report.org = organization; report.asset = asset; report.assessment = assessment; report.vulns = vulnerabilities; if (config && config.companyName) { report.companyName = config.companyName; } else { report.companyName = null; } return res.status(200).json(report); } catch (error) { console.error('Error generating report:', error); return res.status(500).json('An error occurred while generating the report'); } }; /** * @description Delete assessment by ID * @param {UserRequest} req vulnID is required * @param {Response} res contains JSON object with the success/fail * @returns success/error message */ export const deleteAssessmentById = async (req: UserRequest, res: Response) => { try { if (!req.params.assessmentId) { return res.status(400).send('Invalid assessment ID'); } if (isNaN(+req.params.assessmentId)) { return res.status(400).send('Invalid assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const assessment = await assessmentRepository.findOne({ where: { id: +req.params.assessmentId }, relations: ['asset'] }); if (!assessment) { return res.status(404).send('Assessment does not exist.'); } const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } await assessmentRepository.remove(assessment); return res .status(200) .json( `Assessment #${assessment.id}: "${assessment.name}" successfully deleted` ); } catch (error) { console.error('Error deleting assessment:', error); return res.status(500).json('An error occurred while deleting the assessment'); } }; ================================================ FILE: src/routes/asset.controller.spec.ts ================================================ import { Asset } from '../entity/Asset'; import { Organization } from '../entity/Organization'; import * as assetController from './asset.controller'; import { createConnection, getConnection, Entity, getRepository, } from 'typeorm'; import { File } from '../entity/File'; import { Vulnerability } from '../entity/Vulnerability'; import { Assessment } from '../entity/Assessment'; import { User } from '../entity/User'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import MockExpressResponse = require('mock-express-response'); import MockExpressRequest = require('mock-express-request'); import { Jira } from '../entity/Jira'; import { Team } from '../entity/Team'; import { ApiKey } from '../entity/ApiKey'; describe('Asset Controller', () => { beforeEach(async () => { await createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Asset, Organization, File, Vulnerability, Assessment, User, ProblemLocation, Resource, Jira, Team, ApiKey, ], synchronize: true, logging: false, name: 'default', }); }); afterEach(() => { const conn = getConnection('default'); return conn.close(); }); test('Activate Asset By ID', async () => { const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { assetId: null, }, }); await assetController.activateAssetById(request, response); expect(response.statusCode).toBe(400); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ params: { assetId: 999, }, }); await assetController.activateAssetById(request2, response2); expect(response2.statusCode).toBe(404); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'AH', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(insertAsset); const response3 = new MockExpressResponse(); const request3 = new MockExpressRequest({ params: { assetId: 1, }, }); await assetController.activateAssetById(request3, response3); expect(response3.statusCode).toBe(200); }); test('Archive Asset By ID', async () => { const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { assetId: null, }, }); await assetController.archiveAssetById(request, response); expect(response.statusCode).toBe(400); const response2 = new MockExpressResponse(); const request2 = new MockExpressRequest({ params: { assetId: 999, }, }); await assetController.archiveAssetById(request2, response2); expect(response2.statusCode).toBe(404); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'A', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(insertAsset); const response3 = new MockExpressResponse(); const request3 = new MockExpressRequest({ params: { assetId: 1, }, }); await assetController.archiveAssetById(request3, response3); expect(response3.statusCode).toBe(200); }); test('Get Active Assets', async () => { const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { id: null, }, }); await assetController.getOrgAssets(request, response); expect(response.statusCode).toBe(400); const request2 = new MockExpressRequest({ params: { id: 'abc', }, }); const response2 = new MockExpressResponse(); await assetController.getOrgAssets(request2, response2); expect(response2.statusCode).toBe(400); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(org); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'A', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset = await getConnection() .getRepository(Asset) .save(insertAsset); const request3 = new MockExpressRequest({ params: { id: savedOrg.id, }, userOrgs: [savedOrg.id], userAssets: [savedAsset.id], }); const response3 = new MockExpressResponse(); await assetController.getOrgAssets(request3, response3); expect(response3._getJSON()).toHaveLength(1); const request4 = new MockExpressRequest({ params: { id: savedOrg.id, }, }); const response4 = new MockExpressResponse(); await assetController.getOrgAssets(request4, response4); expect(response4.statusCode).toBe(404); }); test('Get Archived Assets', async () => { const response = new MockExpressResponse(); const request = new MockExpressRequest({ params: { id: null, }, }); await assetController.getArchivedOrgAssets(request, response); expect(response.statusCode).toBe(400); const request2 = new MockExpressRequest({ params: { id: 'abc', }, }); const response2 = new MockExpressResponse(); await assetController.getArchivedOrgAssets(request2, response2); expect(response2.statusCode).toBe(400); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const insertAsset: Asset = { id: null, name: 'testAsset', status: 'AH', organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset = await getConnection() .getRepository(Asset) .save(insertAsset); const request3 = new MockExpressRequest({ params: { id: 1, }, userOrgs: [savedOrg.id], userAssets: [savedAsset.id], }); const response3 = new MockExpressResponse(); await assetController.getArchivedOrgAssets(request3, response3); expect(response3._getJSON()).toHaveLength(1); const request4 = new MockExpressRequest({ params: { id: savedOrg.id, }, }); const response4 = new MockExpressResponse(); await assetController.getArchivedOrgAssets(request4, response4); expect(response4.statusCode).toBe(404); }); test('Purge jira info success', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); let savedAsset = await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); const jira: Jira = { id: null, username: 'test', host: 'test', apiKey: 'test', asset: null, }; await getConnection().getRepository(Asset).save(savedAsset); savedAsset = await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); jira.asset = savedAsset; await getConnection().getRepository(Jira).insert(jira); const request = new MockExpressRequest({ params: { assetId: 1, }, }); const response = new MockExpressResponse(); await assetController.purgeJiraInfo(request, response); expect(response.statusCode).toBe(200); }); test('Purge jira info failure', async () => { const request = new MockExpressRequest({ params: { id: 1, }, }); const response = new MockExpressResponse(); await assetController.purgeJiraInfo(request, response); expect(response.statusCode).toBe(400); const request2 = new MockExpressRequest({ params: { assetId: 'test', }, }); const response2 = new MockExpressResponse(); await assetController.purgeJiraInfo(request2, response2); expect(response2.statusCode).toBe(400); }); test('Create Asset no jira success', async () => { const request = new MockExpressRequest({ params: { id: 1, }, body: { name: 'test asset', jira: null, }, }); const response = new MockExpressResponse(); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); await getConnection().getRepository(Organization).findOne({ where: { id: 1 } }); await assetController.createAsset(request, response); expect(response.statusCode).toBe(200); }); test('Create Asset with jira success', async () => { const request = new MockExpressRequest({ params: { id: 1, }, body: { name: 'test asset', jira: { username: 'test', apiKey: 'test', host: 'test', }, }, }); const response = new MockExpressResponse(); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); await assetController.createAsset(request, response); expect(response.statusCode).toBe(200); }); test('Create Asset failure org id not valid', async () => { const request = new MockExpressRequest({ params: { badId: 1, }, body: { name: 'test asset', jira: { username: 'test', apiKey: 'test', host: 'test', }, }, }); const response = new MockExpressResponse(); await assetController.createAsset(request, response); expect(response.statusCode).toBe(400); }); test('Create Asset failure org does not exist', async () => { const request = new MockExpressRequest({ params: { id: 1, }, body: { name: 'test asset', jira: { username: 'test', apiKey: 'test', host: 'test', }, }, }); const response = new MockExpressResponse(); await assetController.createAsset(request, response); expect(response.statusCode).toBe(404); }); test('Create Asset failure missing name', async () => { const request = new MockExpressRequest({ params: { id: 1, }, body: { jira: { username: 'test', apiKey: 'test', host: 'test', }, }, }); const response = new MockExpressResponse(); const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); await assetController.createAsset(request, response); expect(response.statusCode).toBe(400); }); test('Get asset by id success', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); const request = new MockExpressRequest({ params: { assetId: 1, }, userAssets: [1], }); const response = new MockExpressResponse(); await assetController.getAssetById(request, response); expect(response.statusCode).toBe(200); }); test('Get asset by id failure no access', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); const request = new MockExpressRequest({ params: { assetId: 1, }, userAssets: [2], }); const response = new MockExpressResponse(); await assetController.getAssetById(request, response); expect(response.statusCode).toBe(404); }); test('get asset by id failure', async () => { const request = new MockExpressRequest({ params: { id: 1, }, }); const response = new MockExpressResponse(); await assetController.getAssetById(request, response); expect(response.statusCode).toBe(400); }); test('Get asset by id failure asset does not exist', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(org); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; const savedAsset = await getConnection().getRepository(Asset).save(asset); const request = new MockExpressRequest({ params: { assetId: 2, }, userAssets: [2], }); const response = new MockExpressResponse(); await assetController.getAssetById(request, response); expect(response.statusCode).toBe(404); }); test('Get asset by id success delete api key', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); const savedAsset = await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); const jira: Jira = { id: null, username: 'test', host: 'test', apiKey: 'test', asset: savedAsset, }; await getConnection().getRepository(Jira).insert(jira); const request = new MockExpressRequest({ params: { assetId: 1, }, userAssets: [1], }); const response = new MockExpressResponse(); await assetController.getAssetById(request, response); expect(response.statusCode).toBe(200); }); test('Update by asset id success', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); const request = new MockExpressRequest({ params: { assetId: 1, }, body: { name: 'updated asset', jira: { id: null, host: 'test', username: 'test', apiKey: 'test', } as Jira, }, }); const response = new MockExpressResponse(); await assetController.updateAssetById(request, response); expect(response.statusCode).toBe(200); }); test('Update asset by id failure asset id is not valid', async () => { const request = new MockExpressRequest({ params: { id: 1, }, body: { name: 'updated asset', jira: { id: null, host: 'test', username: 'test', apiKey: 'test', } as Jira, }, }); const response = new MockExpressResponse(); await assetController.updateAssetById(request, response); expect(response.statusCode).toBe(400); }); test('Update asset by id failure asset does not exist', async () => { const request = new MockExpressRequest({ params: { assetId: 1, }, body: { name: 'updated asset', jira: { id: null, host: 'test', username: 'test', apiKey: 'test', } as Jira, }, }); const response = new MockExpressResponse(); await assetController.updateAssetById(request, response); expect(response.statusCode).toBe(404); }); test('Update by asset id failure missing name', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); const request = new MockExpressRequest({ params: { assetId: 1, }, body: { jira: { id: null, host: 'test', username: 'test', apiKey: 'test', } as Jira, }, }); const response = new MockExpressResponse(); await assetController.updateAssetById(request, response); expect(response.statusCode).toBe(400); }); test('Update by asset id failure jira integration', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; await getConnection().getRepository(Asset).insert(asset); await getConnection().getRepository(Asset).findOne({ where: { id: 1 } }); const request = new MockExpressRequest({ params: { assetId: 1, }, body: { name: 'test', jira: { id: null, host: 1, username: 1, apiKey: 1, }, }, }); const response = new MockExpressResponse(); await assetController.updateAssetById(request, response); expect(response.statusCode).toBe(400); }); test('Get open vulns by asset', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; const savedAsset = await getConnection().getRepository(Asset).save(asset); const request = new MockExpressRequest({ params: { assetId: savedAsset.id, }, userAssets: [savedAsset.id], }); const response = new MockExpressResponse(); await assetController.getOpenVulnsByAsset(request, response); expect(response.statusCode).toBe(200); }); test('Get open vulns by asset', async () => { const org: Organization = { id: null, name: 'testOrg', asset: null, status: 'A', teams: null, }; await getConnection().getRepository(Organization).insert(org); const savedOrg = await getConnection() .getRepository(Organization) .findOne({ where: { id: 1 } }); const assessments: Assessment[] = []; const asset: Asset = { id: null, name: 'Test Asset', status: 'A', assessment: assessments, organization: savedOrg, jira: null, teams: null, }; const savedAsset = await getConnection().getRepository(Asset).save(asset); const request = new MockExpressRequest({ params: { assetId: savedAsset.id, }, userAssets: [999], }); const response = new MockExpressResponse(); await assetController.getOpenVulnsByAsset(request, response); expect(response.statusCode).toBe(404); }); }); ================================================ FILE: src/routes/asset.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { Response, Request } from 'express'; import { AppDataSource } from '../data-source'; import { Asset } from '../entity/Asset'; import { validate } from 'class-validator'; import { Organization } from '../entity/Organization'; import { status } from '../enums/status-enum'; import { encrypt } from '../utilities/crypto.utility'; import { Jira } from '../entity/Jira'; import { hasAssetReadAccess, hasOrgAccess } from '../utilities/role.utility'; import { Vulnerability } from '../entity/Vulnerability'; import { In } from 'typeorm'; /** * @description Get organization assets * @param {UserRequest} req * @param {Response} res * @returns assets */ export const getOrgAssets = async (req: UserRequest, res: Response) => { try { if (!req.params.id) { return res.status(400).json('Invalid Asset Request'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Organization ID'); } if (!hasOrgAccess(req, +req.params.id)) { return res.status(404).json('Organization not found'); } const assetRepository = AppDataSource.getRepository(Asset); // Build query based on database type const userAssetsPlaceholder = AppDataSource.options.type === 'sqlite' ? req.userAssets : [null, ...req.userAssets]; const assets = await assetRepository .createQueryBuilder('asset') .leftJoinAndSelect('asset.jira', 'jira') .where('asset.organizationId = :orgId', { orgId: req.params.id, }) .andWhere('asset.status = :status', { status: status.active, }) .andWhere('asset.id IN (:...userAssets)', { userAssets: userAssetsPlaceholder, }) .select(['asset.id', 'asset.name', 'asset.status', 'jira.id']) .getMany(); if (!assets || assets.length === 0) { return res.status(200).json([]); } // Get open vulnerability counts for each asset for (const asset of assets) { asset['openVulnCount'] = await getOpenVulnCountByAsset(asset); } return res.status(200).json(assets); } catch (error) { console.error('Error fetching organization assets:', error); return res.status(500).json('An error occurred while fetching organization assets'); } }; /** * @description Get organization archived assets * @param {UserRequest} req * @param {Response} res * @returns assets */ export const getArchivedOrgAssets = async (req: UserRequest, res: Response) => { try { if (!req.params.id) { return res.status(400).json('Invalid Asset Request'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Organization ID'); } if (!hasOrgAccess(req, +req.params.id)) { return res.status(404).json('Organization not found'); } const assetRepository = AppDataSource.getRepository(Asset); // Build query based on database type const userAssetsPlaceholder = AppDataSource.options.type === 'sqlite' ? req.userAssets : [null, ...req.userAssets]; const assets = await assetRepository .createQueryBuilder('asset') .leftJoinAndSelect('asset.jira', 'jira') .where('asset.organizationId = :orgId', { orgId: req.params.id, }) .andWhere('asset.status = :status', { status: status.archived, }) .andWhere('asset.id IN (:...userAssets)', { userAssets: userAssetsPlaceholder, }) .select(['asset.id', 'asset.name', 'asset.status', 'jira.id']) .getMany(); if (!assets || assets.length === 0) { return res.status(200).json([]); } // Get open vulnerability counts for each asset for (const asset of assets) { asset['openVulnCount'] = await getOpenVulnCountByAsset(asset); } return res.status(200).json(assets); } catch (error) { console.error('Error fetching archived assets:', error); return res.status(500).json('An error occurred while fetching archived assets'); } }; /** * @description Gets vulnerability count by asset ID * @param {Asset} asset * @returns integer */ export const getOpenVulnCountByAsset = async (asset: Asset): Promise => { try { const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const vulnCount = await vulnerabilityRepository .createQueryBuilder('vuln') .leftJoinAndSelect('vuln.assessment', 'assessment') .leftJoinAndSelect('assessment.asset', 'asset') .where('asset.id = :assetId', { assetId: asset.id, }) .andWhere('vuln.status = :status', { status: 'Open', }) .select(['vuln.id', 'vuln.name', 'assessment.id', 'asset.id']) .getCount(); return vulnCount; } catch (error) { console.error(`Error getting vulnerability count for asset ${asset.id}:`, error); return 0; } }; /** * @description Fetch open vulnerabilities by asset ID * @param {UserRequest} req * @param {Response} res * @returns array of vulnerabilities */ export const getOpenVulnsByAsset = async (req: UserRequest, res: Response) => { try { const assetAccess = await hasAssetReadAccess(req, +req.params.assetId); if (!assetAccess) { return res.status(404).json('Asset not found'); } const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const vulns = await vulnerabilityRepository .createQueryBuilder('vuln') .leftJoinAndSelect('vuln.assessment', 'assessment') .leftJoinAndSelect('assessment.asset', 'asset') .where('asset.id = :assetId', { assetId: req.params.assetId, }) .andWhere('vuln.status = :status', { status: 'Open', }) .select([ 'vuln.id', 'vuln.name', 'vuln.risk', 'vuln.systemic', 'vuln.cvssScore', 'vuln.cvssUrl', 'assessment.id', 'vuln.jiraId', ]) .getMany(); return res.status(200).json(vulns); } catch (error) { console.error('Error fetching open vulnerabilities:', error); return res.status(500).json('An error occurred while fetching open vulnerabilities'); } }; /** * @description API backend for creating an asset associated by org ID * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const createAsset = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.id)) { return res.status(400).json('Organization ID is not valid'); } const organizationRepository = AppDataSource.getRepository(Organization); const assetRepository = AppDataSource.getRepository(Asset); const org = await organizationRepository.findOne({ where: { id: +req.params.id } }); if (!org) { return res.status(404).json('Organization does not exist'); } if (!req.body.name) { return res.status(400).send('Asset is not valid'); } let asset = new Asset(); asset.name = req.body.name; asset.organization = org; asset.status = status.active; const errors = await validate(asset); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Asset form validation failed'); } asset = await assetRepository.save(asset); // Handle Jira integration if provided if ( req.body.jira && req.body.jira.username && req.body.jira.host && req.body.jira.apiKey ) { try { await addJiraIntegration( req.body.jira.username, req.body.jira.host, req.body.jira.apiKey, asset ); return res.status(200).json('Asset saved successfully with Jira integration'); } catch (err) { return res.status(400).json(err); } } else { return res .status(200) .json( 'Asset saved successfully. Unable to integrate Jira. JIRA integration requires username, host, and API key.' ); } } catch (error) { console.error('Error creating asset:', error); return res.status(500).json('An error occurred while creating the asset'); } }; /** * @description Purge JIRA by asset ID * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const purgeJiraInfo = async (req: Request, res: Response) => { try { if (!req.params.assetId) { return res.status(400).json('Asset ID is not valid'); } if (isNaN(+req.params.assetId)) { return res.status(400).json('Asset ID is not valid'); } const assetRepository = AppDataSource.getRepository(Asset); const jiraRepository = AppDataSource.getRepository(Jira); const asset = await assetRepository.findOne({ where: { id: +req.params.assetId }, relations: ['jira'] }); if (!asset || !asset.jira) { return res.status(404).json('Asset or Jira integration not found'); } await jiraRepository.remove(asset.jira); return res.status(200).json('The API Key has been purged successfully'); } catch (error) { console.error('Error purging Jira info:', error); return res.status(500).json('An error occurred while purging Jira info'); } }; /** * @description Associates Asset to JIRA integration * @param {string} username * @param {string} host * @param {string} apiKey * @param {Asset} asset * @returns Promise */ const addJiraIntegration = ( username: string, host: string, apiKey: string, asset: Asset ): Promise => { return new Promise(async (resolve, reject) => { try { const assetRepository = AppDataSource.getRepository(Asset); const jiraRepository = AppDataSource.getRepository(Jira); const existingAsset = await assetRepository.findOne({ where: { id: asset.id }, relations: ['jira'] }); if (existingAsset.jira) { return reject( `The Asset: ${existingAsset.name} contains an existing Jira integration. Purge the existing Jira integration and try again.` ); } const jiraInit: Jira = new Jira(); jiraInit.username = username; jiraInit.host = host; jiraInit.asset = asset; try { jiraInit.apiKey = encrypt(apiKey); } catch (err) { return reject(err); } const errors = await validate(jiraInit); if (errors.length > 0) { console.error('Validation errors:', errors); return reject('Jira integration requires username, host, and API key.'); } const jiraResult = await jiraRepository.save(jiraInit); resolve(jiraResult); } catch (error) { console.error('Error adding Jira integration:', error); reject('An error occurred while adding Jira integration'); } }); }; /** * @description Get asset by ID * @param {UserRequest} req * @param {Response} res * @returns asset */ export const getAssetById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.assetId)) { return res.status(400).json('Invalid Asset ID'); } const assetRepository = AppDataSource.getRepository(Asset); const asset = await assetRepository.findOne({ where: { id: +req.params.assetId }, relations: ['jira'] }); if (!asset) { return res.status(404).send('Asset does not exist'); } const hasAccess = await hasAssetReadAccess(req, asset.id); if (!hasAccess) { return res.status(404).json('Asset not found'); } // Don't return the API key if (asset.jira) { delete asset.jira.apiKey; } return res.status(200).json(asset); } catch (error) { console.error('Error fetching asset:', error); return res.status(500).json('An error occurred while fetching the asset'); } }; /** * @description Update asset by ID * @param {UserRequest} req * @param {Response} res * @return success/error message */ export const updateAssetById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.assetId) || !req.params.assetId) { return res.status(400).json('Asset ID is not valid'); } const assetRepository = AppDataSource.getRepository(Asset); const asset = await assetRepository.findOne({ where: { id: +req.params.assetId } }); if (!asset) { return res.status(404).json('Asset does not exist'); } if (!req.body.name) { return res.status(400).json('Asset name is not valid'); } // Handle Jira integration if provided try { if ( req.body.jira && req.body.jira.username && req.body.jira.host && req.body.jira.apiKey ) { await addJiraIntegration( req.body.jira.username, req.body.jira.host, req.body.jira.apiKey, asset ); } } catch (err) { return res.status(400).json('JIRA integration validation failed'); } asset.name = req.body.name; const errors = await validate(asset, { skipMissingProperties: true }); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Asset form validation failed'); } await assetRepository.save(asset); return res.status(200).json('Asset updated successfully'); } catch (error) { console.error('Error updating asset:', error); return res.status(500).json('An error occurred while updating the asset'); } }; /** * @description Archive asset by ID * @param {UserRequest} req * @param {Response} res * @return success/error message */ export const archiveAssetById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.assetId) || !req.params.assetId) { return res.status(400).json('Asset ID is not valid'); } const assetRepository = AppDataSource.getRepository(Asset); const asset = await assetRepository.findOne({ where: { id: +req.params.assetId } }); if (!asset) { return res.status(404).json('Asset does not exist'); } asset.status = status.archived; await assetRepository.save(asset); return res.status(200).json('Asset archived successfully'); } catch (error) { console.error('Error archiving asset:', error); return res.status(500).json('An error occurred while archiving the asset'); } }; /** * @description Activate an asset by ID * @param {UserRequest} req * @param {Response} res * @return success/error message */ export const activateAssetById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.assetId) || !req.params.assetId) { return res.status(400).json('Asset ID is not valid'); } const assetRepository = AppDataSource.getRepository(Asset); const asset = await assetRepository.findOne({ where: { id: +req.params.assetId } }); if (!asset) { return res.status(404).json('Asset does not exist'); } asset.status = status.active; await assetRepository.save(asset); return res.status(200).json('Asset activated successfully'); } catch (error) { console.error('Error activating asset:', error); return res.status(500).json('An error occurred while activating the asset'); } }; ================================================ FILE: src/routes/authentication.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { AppDataSource } from '../data-source'; import { User } from '../entity/User'; import { v4 as uuidv4 } from 'uuid'; import { Response } from 'express'; import jwt = require('jsonwebtoken'); import { generateHash, passwordSchema, compare, } from '../utilities/password.utility'; import { passwordRequirement } from '../enums/message-enum'; import * as emailService from '../services/email.service'; import { Config } from '../entity/Config'; import { ROLE } from '../enums/roles-enum'; /** * @description Login to the application * @param {UserRequest} req * @param {Response} res * @returns valid JWT token */ const login = async (req: UserRequest, res: Response) => { try { const { password, email } = req.body; if (!email || !password) { return res.status(400).json('Email and password are required'); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.teams', 'team') .where('user.email = :email', { email, }) .getOne(); if (!user) { return res.status(400).json('Invalid email or password'); } if (!user.active) { // Generate new UUID for verification user.uuid = uuidv4(); await userRepository.save(user); // Check if email config exists const configRepository = AppDataSource.getRepository(Config); const config = await configRepository.findOne({ where: { id: 1 } }); if (!config || !config.fromEmail || !config.fromEmailPassword) { emailService.sendVerificationEmail(user.uuid, user.email); return res .status(400) .json( 'This account has not been activated. The email configuration has not been set. Please contact an administrator' ); } return res .status(400) .json( 'This account has not been activated. Please check for email verification or contact an administrator.' ); } // Verify password let valid: boolean; try { valid = await compare(password, user.password); } catch (err) { console.error('Password comparison error:', err); return res.status(400).json('Authentication failed'); } if (valid) { const tokens = generateTokens(user); return res.status(200).json(tokens); } else { return res.status(400).json('Invalid email or password'); } } catch (error) { console.error('Login error:', error); return res.status(500).json('An error occurred during login'); } }; /** * @description Initiates forgot password process * @param {UserRequest} req * @param {Response} res * @returns Success message */ const forgotPassword = async (req: UserRequest, res: Response) => { try { const { email } = req.body; if (!email) { return res.status(400).json('Email is invalid'); } const userRepository = AppDataSource.getRepository(User); const configRepository = AppDataSource.getRepository(Config); // Always return the same response regardless of whether the user exists // This prevents user enumeration attacks const successMessage = 'A password reset request has been initiated. Please check your email.'; const user = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email, }) .getOne(); if (!user) { return res.status(200).json(successMessage); } // Generate and save UUID for password reset user.uuid = uuidv4(); await userRepository.save(user); // Check if email config exists const config = await configRepository.findOne({ where: { id: 1 } }); if (!config || !config.fromEmail || !config.fromEmailPassword) { emailService.sendForgotPasswordEmail(user.uuid, user.email); return res .status(400) .json( 'Unable to initiate the password reset process. The email configuration has not been set. Please contact an administrator.' ); } // Send email and return success emailService.sendForgotPasswordEmail(user.uuid, user.email); return res.status(200).json(successMessage); } catch (error) { console.error('Forgot password error:', error); return res.status(500).json('An error occurred during the password reset process'); } }; /** * @description Reset user password * @param {UserRequest} req * @param {Response} res * @returns Success message */ const resetPassword = async (req: UserRequest, res: Response) => { try { const { password, confirmPassword, uuid } = req.body; if (!password || !confirmPassword || !uuid) { return res.status(400).json('Missing required fields'); } if (password !== confirmPassword) { return res.status(400).json('Passwords do not match'); } if (!passwordSchema.validate(password)) { return res.status(400).json(passwordRequirement); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository .createQueryBuilder('user') .where('user.uuid = :uuid', { uuid, }) .getOne(); if (!user) { return res .status(400) .json( 'Unable to reset user password at this time. Please contact an administrator for assistance.' ); } user.password = await generateHash(password); user.uuid = null; // Clear the reset token after use await userRepository.save(user); return res.status(200).json('Password updated successfully'); } catch (error) { console.error('Reset password error:', error); return res.status(500).json('An error occurred during password reset'); } }; /** * @description Refresh Session * @param {UserRequest} req * @param {Response} res * @returns Tokens */ const refreshSession = async (req: UserRequest, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.teams', 'team') .where('user.id = :userId', { userId: req.user, }) .getOne(); if (!user) { return res.status(404).json('User not found'); } const tokens = generateTokens(user); return res.status(200).json(tokens); } catch (error) { console.error('Session refresh error:', error); return res.status(500).json('An error occurred while refreshing the session'); } }; /** * @description Generate Tokens * @param {User} user * @returns JWT tokens */ const generateTokens = (user: User) => { const isAdmin = user.teams && user.teams.some((team) => team.role === ROLE.ADMIN); const token = jwt.sign( { userId: user.id, admin: isAdmin }, process.env.JWT_KEY, { expiresIn: '15m' } ); const refreshToken = jwt.sign( { userId: user.id }, process.env.JWT_REFRESH_KEY, { expiresIn: '8h' } ); return { token, refreshToken, }; }; module.exports = { login, forgotPassword, resetPassword, refreshSession, }; ================================================ FILE: src/routes/config.controller.spec.ts ================================================ import { createConnection, getConnection } from 'typeorm'; import * as configController from './config.controller'; import MockExpressResponse = require('mock-express-response'); import MockExpressRequest = require('mock-express-request'); import { User } from '../entity/User'; import { Team } from '../entity/Team'; import { compare } from '../utilities/password.utility'; import { v4 as uuidv4 } from 'uuid'; import { Assessment } from '../entity/Assessment'; import { Asset } from '../entity/Asset'; import { Jira } from '../entity/Jira'; import { Organization } from '../entity/Organization'; import { ProblemLocation } from '../entity/ProblemLocation'; import { ReportAudit } from '../entity/ReportAudit'; import { Resource } from '../entity/Resource'; import { Vulnerability } from '../entity/Vulnerability'; import { Config } from '../entity/Config'; import { File } from '../entity/File'; import { ApiKey } from '../entity/ApiKey'; describe('config controller', () => { beforeEach(() => { return createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Config, User, Team, Organization, Asset, Assessment, Vulnerability, ProblemLocation, ReportAudit, Resource, Jira, File, ApiKey, ], synchronize: true, logging: false, }); }); afterEach(() => { const conn = getConnection(); return conn.close(); }); test('initalize config', async () => { await configController.initialInsert(); const userAry = await getConnection().getRepository(User).find({}); const configAry = await getConnection().getRepository(Config).find({}); const teamAry = await getConnection() .getRepository(Team) .find({ relations: ['users'] }); expect(userAry.length).toBe(1); expect(configAry.length).toBe(1); expect(userAry[0].id).toBe(1); expect(userAry[0].firstName).toBe('Master'); expect(userAry[0].lastName).toBe('Chief'); expect(userAry[0].email).toBe('admin@example.com'); expect(userAry[0].title).toBe('Spartan 117'); expect(userAry[0].active).toBeTruthy(); expect(teamAry[0].name).toBe('Administrators'); expect(teamAry[0].users[0].email).toBe('admin@example.com'); expect(teamAry[0].createdBy && teamAry[0].lastUpdatedBy).toBe(1); const initUsrPw = userAry[0].password; expect(compare('changeMe', initUsrPw)).toBeTruthy(); }); test('initial user exists. teams do not exist', async () => { const user = new User(); user.active = false; user.uuid = uuidv4(); user.email = 'testing@jest.com'; user.newEmail = null; await getConnection().getRepository(User).save(user); await configController.initialInsert(); const teamAry = await getConnection() .getRepository(Team) .find({ relations: ['users'] }); expect(teamAry[0].users).toHaveLength(0); expect(teamAry[0].createdBy && teamAry[0].lastUpdatedBy).toBeNull(); }); test('save configuration success', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = '123'; config.id = 1; await getConnection().getRepository(Config).insert(config); const response = new MockExpressResponse(); const request = new MockExpressRequest({ body: { fromEmail: 'test@jest.com', fromEmailPassword: 'abc123', companyName: 'UNFC', }, }); await configController.saveConfig(request, response); expect(response.statusCode).toBe(200); }); test('save configuration fail validation', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = '123'; config.id = 1; await getConnection().getRepository(Config).insert(config); const response = new MockExpressResponse(); const request = new MockExpressRequest({ body: { fromEmail: 'test', fromEmailPassword: 'abc123', companyName: 'UNFC', }, }); await configController.saveConfig(request, response); expect(response.statusCode).toBe(400); }); test('patch configuration success', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = '123'; config.id = 1; await getConnection().getRepository(Config).insert(config); const response = new MockExpressResponse(); const request = new MockExpressRequest({ body: { fromEmail: 'test@jest.com', fromEmailPassword: 'abc123', companyName: 'UNFC', }, }); await configController.saveConfig(request, response); expect(response.statusCode).toBe(200); }); test('patch configuration fail validation', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = '123'; config.id = 1; await getConnection().getRepository(Config).insert(config); const response = new MockExpressResponse(); const request = new MockExpressRequest({ body: { fromEmail: 'test', fromEmailPassword: 'abc123', companyName: 'UNFC', }, }); await configController.saveConfig(request, response); expect(response.statusCode).toBe(400); }); }); ================================================ FILE: src/routes/config.controller.ts ================================================ import { Response, Request } from 'express'; import { Config } from '../entity/Config'; import { User } from '../entity/User'; import { Team } from '../entity/Team'; import { encrypt } from '../utilities/crypto.utility'; import { validate } from 'class-validator'; import { generateHash } from '../utilities/password.utility'; import { ROLE } from '../enums/roles-enum'; import { AppDataSource } from '../data-source'; export const initialInsert = async () => { try { const configRepository = AppDataSource.getRepository(Config); const userRepository = AppDataSource.getRepository(User); const teamRepository = AppDataSource.getRepository(Team); const configAry = await configRepository.find(); const usrAry = await userRepository.find(); let savedUser: User; const defaultTeamAry = await teamRepository.find(); // Create initial configuration if it doesn't exist if (!configAry.length) { const initialConfig = new Config(); initialConfig.companyName = null; initialConfig.fromEmail = null; initialConfig.fromEmailPassword = null; await configRepository.save(initialConfig); } // Create initial admin user if no users exist if (!usrAry.length) { const initialUser = new User(); initialUser.active = true; initialUser.email = 'admin@example.com'; initialUser.firstName = 'Master'; initialUser.lastName = 'Chief'; initialUser.title = 'Spartan 117'; initialUser.password = await generateHash('changeMe'); savedUser = await userRepository.save(initialUser); } // Create default admin team if no teams exist if (!defaultTeamAry.length) { const defaultAdminTeam = new Team(); defaultAdminTeam.name = 'Administrators'; defaultAdminTeam.createdDate = new Date(); defaultAdminTeam.lastUpdatedDate = new Date(); defaultAdminTeam.createdBy = savedUser ? savedUser.id : null; defaultAdminTeam.lastUpdatedBy = savedUser ? savedUser.id : null; defaultAdminTeam.organization = null; defaultAdminTeam.role = ROLE.ADMIN; if (savedUser) { defaultAdminTeam.users = [savedUser]; } await teamRepository.save(defaultAdminTeam); } } catch (error) { console.error('Error during initial setup:', error); } }; export const getConfig = async (req: Request, res: Response) => { try { const configRepository = AppDataSource.getRepository(Config); const existingConfig = await configRepository.findOne({ where: { id: 1 } }); if (!existingConfig) { return res.status(404).json('Configuration not found'); } // Don't return the password in the response const configResponse = { ...existingConfig }; delete configResponse.fromEmailPassword; return res.status(200).json(configResponse); } catch (error) { console.error('Error getting configuration:', error); return res.status(500).json('An error occurred while fetching the configuration'); } }; export const saveConfig = async (req: Request, res: Response) => { try { const configRepository = AppDataSource.getRepository(Config); const fromEmail = req.body.fromEmail; const fromEmailPassword = req.body.fromEmailPassword; const companyName = req.body.companyName; const existingConfig = await configRepository.findOne({ where: { id: 1 } }); if (!existingConfig) { return res.status(404).json('Configuration not found'); } existingConfig.companyName = companyName; existingConfig.fromEmail = fromEmail; if (fromEmailPassword) { const encryptedPassword = encrypt(fromEmailPassword); existingConfig.fromEmailPassword = encryptedPassword; } const errors = await validate(existingConfig, { skipMissingProperties: true, }); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Settings validation failed'); } await configRepository.save(existingConfig); return res.status(200).json('Settings updated successfully'); } catch (error) { console.error('Error saving configuration:', error); return res.status(500).json('An error occurred while saving the configuration'); } }; ================================================ FILE: src/routes/file-upload.controller.ts ================================================ import { upload, uploadArray } from '../utilities/file.utility'; import { Response, Request } from 'express'; import { UserRequest } from '../interfaces/user-request.interface'; import { File } from '../entity/File'; import { AppDataSource } from '../data-source'; /** * @description Upload a file * @param {UserRequest} req * @param {Response} res * @returns file ID */ const uploadFile = (req: UserRequest, res: Response) => { // TODO: Virus Scanning upload(req, res, async err => { if (req.fileExtError) { return res.status(400).send(req.fileExtError); } if (err) { console.error('File upload error:', err); if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).send('Only File size up to 5 MB allowed'); } return res.status(400).send('File upload failed'); } if (!req.file) { return res.status(400).send('You must provide a file'); } try { const fileRepository = AppDataSource.getRepository(File); let file = new File(); file = req.file; const newFile = await fileRepository.save(file); return res.json(newFile.id); } catch (error) { console.error('Error saving file:', error); return res.status(500).send('Failed to save file'); } }); }; /** * @description Upload multiple files * @param {UserRequest} req * @param {Response} res * @returns file ID */ const uploadFileArray = (req: UserRequest, res: Response) => { return new Promise((resolve, reject) => { uploadArray(req, res, async err => { if (req.fileExtError) { res.status(400).send(req.fileExtError); return reject(req.fileExtError); } if (err) { console.error('File upload error:', err); if (err.code === 'LIMIT_FILE_SIZE') { res.status(400).send('Only File size up to 5 MB allowed'); return reject('File size limit exceeded'); } res.status(400).send('File upload failed'); return reject('File upload failed'); } resolve(req); }); }); }; /** * @description Get file by ID * @param {UserRequest} req * @param {Response} res * @returns a buffer with the file data */ const getFileById = async (req: Request, res: Response) => { try { if (!req.params.id) { return res.status(400).json('Invalid File Request'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid File ID'); } const fileRepository = AppDataSource.getRepository(File); const file = await fileRepository.findOne({ where: { id: +req.params.id } }); if (!file) { return res.status(404).json('File not found'); } res.send(file.buffer); } catch (error) { console.error('Error retrieving file:', error); return res.status(500).json('An error occurred while retrieving the file'); } }; module.exports = { uploadFile, uploadFileArray, getFileById }; ================================================ FILE: src/routes/organization.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { Response } from 'express'; import { AppDataSource } from '../data-source'; import { Organization } from '../entity/Organization'; import { status } from '../enums/status-enum'; import { validate } from 'class-validator'; import { In } from 'typeorm'; /** * @description Get active organizations * @param {UserRequest} req * @param {Response} res * @returns active organizations */ export const getActiveOrgs = async (req: UserRequest, res: Response) => { try { const organizationRepository = AppDataSource.getRepository(Organization); const orgs = await organizationRepository.find({ where: { status: status.active, id: In(req.userOrgs) } }); if (!orgs || orgs.length === 0) { return res.status(200).json([]); } return res.status(200).json(orgs); } catch (error) { console.error('Error fetching active organizations:', error); return res.status(500).json('An error occurred while fetching active organizations'); } }; /** * @description Get archived organizations * @param {UserRequest} req * @param {Response} res * @returns archived organizations */ export const getArchivedOrgs = async (req: UserRequest, res: Response) => { try { const organizationRepository = AppDataSource.getRepository(Organization); const orgs = await organizationRepository.find({ where: { status: status.archived, id: In(req.userOrgs) } }); if (!orgs || orgs.length === 0) { return res.status(200).json([]); } return res.status(200).json(orgs); } catch (error) { console.error('Error fetching archived organizations:', error); return res.status(500).json('An error occurred while fetching archived organizations'); } }; /** * @description Get organization by ID * @param {UserRequest} req * @param {Response} res * @returns organization */ export const getOrgById = async (req: UserRequest, res: Response) => { try { if (!req.params.id) { return res.status(400).json('Invalid Organization Request'); } if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Organization ID'); } // Check if user has access to this organization if (!req.userOrgs.includes(+req.params.id)) { return res.status(404).json('Organization not found'); } const organizationRepository = AppDataSource.getRepository(Organization); const org = await organizationRepository.findOne({ where: { id: +req.params.id } }); if (!org) { return res.status(404).json('Organization does not exist'); } return res.status(200).json({ name: org.name }); } catch (error) { console.error('Error fetching organization:', error); return res.status(500).json('An error occurred while fetching the organization'); } }; /** * @description Archive organization by ID * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const archiveOrgById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.id)) { return res.status(400).json('Invalid Organization ID'); } const organizationRepository = AppDataSource.getRepository(Organization); const org = await organizationRepository.findOne({ where: { id: +req.params.id } }); if (!org) { return res.status(404).json('Organization does not exist'); } org.status = status.archived; const errors = await validate(org); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Organization archive validation failed'); } await organizationRepository.save(org); return res.status(200).json('Organization archived successfully'); } catch (error) { console.error('Error archiving organization:', error); return res.status(500).json('An error occurred while archiving the organization'); } }; /** * @description Activate organization by ID * @param {UserRequest} req * @param {Response} res * @returns success/error */ export const activateOrgById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.id)) { return res.status(400).json('Organization ID is not valid'); } const organizationRepository = AppDataSource.getRepository(Organization); const org = await organizationRepository.findOne({ where: { id: +req.params.id } }); if (!org) { return res.status(404).json('Organization does not exist'); } org.status = status.active; const errors = await validate(org); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Organization activation validation failed'); } await organizationRepository.save(org); return res.status(200).json('Organization activated successfully'); } catch (error) { console.error('Error activating organization:', error); return res.status(500).json('An error occurred while activating the organization'); } }; /** * @description Update organization by ID * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const updateOrgById = async (req: UserRequest, res: Response) => { try { if (isNaN(+req.params.id)) { return res.status(400).json('Organization ID is not valid'); } const organizationRepository = AppDataSource.getRepository(Organization); const org = await organizationRepository.findOne({ where: { id: +req.params.id } }); if (!org) { return res.status(404).json('Organization does not exist'); } org.name = req.body.name; const errors = await validate(org); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Organization form validation failed'); } await organizationRepository.save(org); return res.status(200).json('Organization updated successfully'); } catch (error) { console.error('Error updating organization:', error); return res.status(500).json('An error occurred while updating the organization'); } }; /** * @description Create organization * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const createOrg = async (req: UserRequest, res: Response) => { try { const organizationRepository = AppDataSource.getRepository(Organization); const org = new Organization(); org.name = req.body.name; org.status = status.active; const errors = await validate(org); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Organization form validation failed'); } await organizationRepository.save(org); return res.status(200).json('Organization created successfully'); } catch (error) { console.error('Error creating organization:', error); return res.status(500).json('An error occurred while creating the organization'); } }; ================================================ FILE: src/routes/report-audit.controller.spec.ts ================================================ import { createConnection, getConnection, Entity, getRepository, } from 'typeorm'; import { ReportAudit } from '../entity/ReportAudit'; import { insertReportAuditRecord } from './report-audit.controller'; import { User } from '../entity/User'; import { Assessment } from '../entity/Assessment'; import { Organization } from '../entity/Organization'; import { Vulnerability } from '../entity/Vulnerability'; import { Asset } from '../entity/Asset'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import { File } from '../entity/File'; import { Jira } from '../entity/Jira'; import { v4 as uuidv4 } from 'uuid'; import { generateHash } from '../utilities/password.utility'; import MockExpressRequest = require('mock-express-request'); import { Team } from '../entity/Team'; import { ApiKey } from '../entity/ApiKey'; describe('Report Audit Controller', () => { beforeEach(() => { return createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ ReportAudit, User, File, Assessment, Asset, Organization, Jira, Vulnerability, ProblemLocation, Resource, Team, ApiKey, ], synchronize: true, logging: false, }); }); afterEach(() => { const conn = getConnection(); return conn.close(); }); test('insertReportAuditRecord', async () => { const req = new MockExpressRequest(); const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const savedUser = await getConnection().getRepository(User).save(existUser); const assessment: Assessment = { id: null, name: 'testAssessment', executiveSummary: '', jiraId: '', testUrl: '', prodUrl: '', scope: '', tag: '', startDate: new Date(), endDate: new Date(), asset: new Asset(), testers: null, vulnerabilities: null, }; const savedAssessment = await getConnection() .getRepository(Assessment) .save(assessment); req.user = savedUser.id; const auditRecord = await insertReportAuditRecord( req.user, savedAssessment.id ); const savedAuditRecord = await getConnection() .getRepository(ReportAudit) .findOne({ where: { id: auditRecord.id } }); expect(savedAuditRecord.assessmentId).toBe(savedAssessment.id); }); }); ================================================ FILE: src/routes/report-audit.controller.ts ================================================ import { AppDataSource } from '../data-source'; import { ReportAudit } from '../entity/ReportAudit'; /** * @description Insert report generation audit record * @param {number} userId - The ID of the user generating the report * @param {number} assessmentId - The ID of the assessment for the report * @returns {Promise} - The created report audit record */ export const insertReportAuditRecord = (userId: number, assessmentId: number): Promise => { return new Promise(async (resolve, reject) => { try { const reportAuditRepository = AppDataSource.getRepository(ReportAudit); // Create the report audit record const reportAudit: ReportAudit = new ReportAudit(); reportAudit.generatedBy = userId; reportAudit.assessmentId = assessmentId; reportAudit.generatedDate = new Date(); // Save the report audit record const newAudit = await reportAuditRepository.save(reportAudit); console.info( `Report generated by user ID: ${newAudit.generatedBy} for assessment ID: ${newAudit.assessmentId} on ${newAudit.generatedDate}` ); resolve(newAudit); } catch (err) { console.error('Error inserting report audit record:', err); reject(err); } }); }; ================================================ FILE: src/routes/team.controller.spec.ts ================================================ import { createConnection, getConnection } from 'typeorm'; import { ReportAudit } from '../entity/ReportAudit'; import { User } from '../entity/User'; import { Assessment } from '../entity/Assessment'; import { Organization } from '../entity/Organization'; import { Vulnerability } from '../entity/Vulnerability'; import { Asset } from '../entity/Asset'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import { File } from '../entity/File'; import { Jira } from '../entity/Jira'; import { Team } from '../entity/Team'; import { v4 as uuidv4 } from 'uuid'; import { generateHash } from '../utilities/password.utility'; import MockExpressRequest = require('mock-express-request'); import MockExpressResponse = require('mock-express-response'); import { ROLE } from '../enums/roles-enum'; import { status } from '../enums/status-enum'; import { ApiKey } from '../entity/ApiKey'; import { createTeam, addTeamMember, removeTeamMember, deleteTeam, updateTeamInfo, getMyTeams, addTeamAsset, removeTeamAsset, getAllTeams, } from '../routes/team.controller'; describe('Team Controller', () => { beforeEach(() => { return createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ ReportAudit, User, File, Assessment, Asset, Organization, Jira, Vulnerability, ProblemLocation, Resource, Team, ApiKey, ], synchronize: true, logging: false, }); }); afterEach(() => { const conn = getConnection(); return conn.close(); }); test('Create Team', async () => { const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const savedUser = await getConnection().getRepository(User).save(existUser); const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); // create assets const asset: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset = await getConnection().getRepository(Asset).save(asset); const asset2: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset2 = await getConnection().getRepository(Asset).save(asset2); const asset3: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset3 = await getConnection().getRepository(Asset).save(asset3); const assetAry: Asset[] = []; const request = new MockExpressRequest({ body: { id: null, name: 'Test Team', organization: savedOrg.id, asset: null, role: ROLE.ADMIN, assetIds: [savedAsset.id, savedAsset2.id], users: [savedUser.id], }, user: savedUser.id, }); const response = new MockExpressResponse(); await createTeam(request, response); expect(response.statusCode).toBe(200); const teams = await getConnection() .getRepository(Team) .find({ relations: ['users', 'assets'] }); expect(teams.length).toBe(1); expect(teams[0].users.length).toBe(1); expect(teams[0].assets.length).toBe(2); }); test('Create Team Failure', async () => { const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const savedUser = await getConnection().getRepository(User).save(existUser); const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); const badRequest = new MockExpressRequest({ params: { id: 1, }, body: { id: null, name: 'test', organization: savedOrg.id, asset: null, role: 'not a role', users: [], }, user: savedUser.id, }); const badResponse = new MockExpressResponse(); await createTeam(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const teams = await getConnection().getRepository(Team).find({}); expect(teams.length).toBe(0); const badRequest2 = new MockExpressRequest({ params: { id: 1, }, body: { id: null, name: 'test', organization: 3, asset: null, role: 'not a role', users: [], }, user: savedUser.id, }); const badResponse2 = new MockExpressResponse(); await createTeam(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); }); test('add team member', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); // user 1 const teamMember1 = new User(); teamMember1.firstName = 'master'; teamMember1.lastName = 'chief'; teamMember1.email = 'testing1@jest.com'; teamMember1.active = true; const uuid = uuidv4(); teamMember1.uuid = uuid; teamMember1.password = await generateHash('TangoDown123!!!'); const addedUser1 = await getConnection() .getRepository(User) .save(teamMember1); // user 2 const teamMember2 = new User(); teamMember2.firstName = 'master'; teamMember2.lastName = 'chief'; teamMember2.email = 'testing2@jest.com'; teamMember2.active = true; const uuid2 = uuidv4(); teamMember2.uuid = uuid2; teamMember2.password = await generateHash('TangoDown123!!!'); const addedUser2 = await getConnection() .getRepository(User) .save(teamMember2); // user 3 const teamMember3 = new User(); teamMember3.firstName = 'master'; teamMember3.lastName = 'chief'; teamMember3.email = 'testing3@jest.com'; teamMember3.active = true; const uuid3 = uuidv4(); teamMember3.uuid = uuid3; teamMember3.password = await generateHash('TangoDown123!!!'); const addedUser3 = await getConnection() .getRepository(User) .save(teamMember3); // create team const bravoTeam = new Team(); bravoTeam.name = 'Bravo'; bravoTeam.organization = savedOrg; bravoTeam.id = null; bravoTeam.createdDate = new Date(); bravoTeam.lastUpdatedDate = new Date(); bravoTeam.createdBy = 0; bravoTeam.lastUpdatedBy = 0; bravoTeam.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(bravoTeam); const request = new MockExpressRequest({ body: { userIds: [1, 2, 3], teamId: savedTeam.id, }, }); const response = new MockExpressResponse(); await addTeamMember(request, response); expect(response.statusCode).toBe(200); const badRequest2 = new MockExpressRequest({ body: { userIds: [1, 2, 3], teamId: 'lol', }, }); const badResponse2 = new MockExpressResponse(); await addTeamMember(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); const badRequest3 = new MockExpressRequest({ body: { userIds: [1, 2, 3], }, }); const badResponse3 = new MockExpressResponse(); await addTeamMember(badRequest3, badResponse3); expect(badResponse3.statusCode).toBe(400); const team = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['users'] }); expect(team.users.length).toBe(3); }); test('remove team member', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); // user 1 const teamMember1 = new User(); teamMember1.firstName = 'master'; teamMember1.lastName = 'chief'; teamMember1.email = 'testing1@jest.com'; teamMember1.active = true; const uuid = uuidv4(); teamMember1.uuid = uuid; teamMember1.password = await generateHash('TangoDown123!!!'); const addedUser1 = await getConnection() .getRepository(User) .save(teamMember1); // user 2 const teamMember2 = new User(); teamMember2.firstName = 'master'; teamMember2.lastName = 'chief'; teamMember2.email = 'testing2@jest.com'; teamMember2.active = true; const uuid2 = uuidv4(); teamMember2.uuid = uuid2; teamMember2.password = await generateHash('TangoDown123!!!'); const addedUser2 = await getConnection() .getRepository(User) .save(teamMember2); // user 3 const teamMember3 = new User(); teamMember3.firstName = 'master'; teamMember3.lastName = 'chief'; teamMember3.email = 'testing3@jest.com'; teamMember3.active = true; const uuid3 = uuidv4(); teamMember3.uuid = uuid3; teamMember3.password = await generateHash('TangoDown123!!!'); const addedUser3 = await getConnection() .getRepository(User) .save(teamMember3); // create team const bravoTeam = new Team(); bravoTeam.name = 'Bravo'; bravoTeam.organization = savedOrg; bravoTeam.id = null; bravoTeam.createdDate = new Date(); bravoTeam.lastUpdatedDate = new Date(); bravoTeam.createdBy = 0; bravoTeam.lastUpdatedBy = 0; bravoTeam.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(bravoTeam); let fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['users'] }); expect(fetchTeam.users.length).toBe(0); fetchTeam.users.push(addedUser1, addedUser2, addedUser3); fetchTeam = await getConnection().getRepository(Team).save(fetchTeam); fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: fetchTeam.id }, relations: ['users'] }); expect(fetchTeam.users.length).toBe(3); const request = new MockExpressRequest({ body: { userIds: [1], teamId: savedTeam.id, }, }); const response = new MockExpressResponse(); await removeTeamMember(request, response); expect(response.statusCode).toBe(200); fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: fetchTeam.id }, relations: ['users'] }); expect(fetchTeam.users.length).toBe(2); const badRequest = new MockExpressRequest({ body: { userIds: [1], }, }); const badResponse = new MockExpressResponse(); await removeTeamMember(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const badRequest2 = new MockExpressRequest({ body: { userIds: [1], teamId: 6, }, }); const badResponse2 = new MockExpressResponse(); await removeTeamMember(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); const badRequest3 = new MockExpressRequest({ body: { userIds: [2, 6], teamId: savedTeam.id, }, }); const badResponse3 = new MockExpressResponse(); await removeTeamMember(badRequest3, badResponse3); expect(badResponse3.statusCode).toBe(404); }); test('update team info', async () => { // create user const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const savedUser = await getConnection().getRepository(User).save(existUser); // create user const existUser2 = new User(); existUser2.firstName = 'master'; existUser2.lastName = 'chief'; existUser2.email = 'testing2@jest.com'; existUser2.active = true; const uuid2 = uuidv4(); existUser2.uuid = uuid2; existUser2.password = await generateHash('TangoDown123!!!'); const savedUser2 = await getConnection() .getRepository(User) .save(existUser2); // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); // create assets const asset: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset = await getConnection().getRepository(Asset).save(asset); const asset2: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset2 = await getConnection().getRepository(Asset).save(asset2); const asset3: Asset = { organization: savedOrg, name: 'Test Asset 1', status: 'A', id: null, jira: null, assessment: null, teams: null, }; const savedAsset3 = await getConnection().getRepository(Asset).save(asset3); const assetAry: Asset[] = []; assetAry.push(savedAsset, savedAsset2); // create team const bravoTeam = new Team(); bravoTeam.name = 'Bravo'; bravoTeam.organization = savedOrg; bravoTeam.id = null; bravoTeam.createdDate = new Date(); bravoTeam.lastUpdatedDate = new Date(); bravoTeam.createdBy = 0; bravoTeam.lastUpdatedBy = 0; bravoTeam.role = ROLE.READONLY; bravoTeam.assets = assetAry; bravoTeam.users = [savedUser]; const savedTeam = await getConnection().getRepository(Team).save(bravoTeam); expect(savedTeam.assets.length).toBe(2); expect(savedTeam.users.length).toBe(1); const request = new MockExpressRequest({ body: { name: 'Alpha', organization: savedOrg.id, asset: null, role: ROLE.TESTER, id: savedTeam.id, users: [savedUser, savedUser2], assetIds: [], }, }); const response = new MockExpressResponse(); await updateTeamInfo(request, response); expect(response.statusCode).toBe(200); const updatedTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['users', 'assets'] }); expect(updatedTeam.name).toBe('Alpha'); expect(updatedTeam.role).toBe(ROLE.TESTER); expect(updatedTeam.users.length).toBe(2); expect(updatedTeam.assets.length).toBe(0); const badRequest = new MockExpressRequest({ body: { id: savedTeam.id, }, }); const badResponse = new MockExpressResponse(); await updateTeamInfo(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const badRequest2 = new MockExpressRequest({ body: { name: 'Alpha', organization: savedOrg.id, assetIds: [], role: ROLE.TESTER, }, }); const badResponse2 = new MockExpressResponse(); await updateTeamInfo(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(400); const badRequest3 = new MockExpressRequest({ body: { name: 'Alpha', organization: savedOrg.id, assetIds: [], role: ROLE.TESTER, id: 6, }, }); const badResponse3 = new MockExpressResponse(); await updateTeamInfo(badRequest3, badResponse3); expect(badResponse3.statusCode).toBe(404); expect(badResponse2.statusCode).toBe(400); const badRequest4 = new MockExpressRequest({ body: { name: 'Alpha', organization: savedOrg.id, assetIds: [], role: 'not a role', id: savedTeam.id, }, }); const badResponse4 = new MockExpressResponse(); await updateTeamInfo(badRequest4, badResponse4); expect(badResponse4.statusCode).toBe(400); }); test('delete team', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); // user 1 const teamMember1 = new User(); teamMember1.firstName = 'master'; teamMember1.lastName = 'chief'; teamMember1.email = 'testing1@jest.com'; teamMember1.active = true; const uuid = uuidv4(); teamMember1.uuid = uuid; teamMember1.password = await generateHash('TangoDown123!!!'); const addedUser1 = await getConnection() .getRepository(User) .save(teamMember1); // create team const bravoTeam = new Team(); bravoTeam.name = 'Bravo'; bravoTeam.organization = savedOrg; bravoTeam.id = null; bravoTeam.createdDate = new Date(); bravoTeam.lastUpdatedDate = new Date(); bravoTeam.createdBy = 0; bravoTeam.lastUpdatedBy = 0; bravoTeam.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(bravoTeam); const fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['users'] }); fetchTeam.users.push(addedUser1); const savedTeamWithUser = await getConnection() .getRepository(Team) .save(fetchTeam); const userWithTeam = await getConnection() .getRepository(User) .findOne({ where: { id: addedUser1.id }, relations: ['teams'] }); expect(userWithTeam.teams.length).toBe(1); let teams = await getConnection().getRepository(Team).find({}); expect(teams.length).toBe(1); const request = new MockExpressRequest({ params: { teamId: savedTeamWithUser.id, }, }); const response = new MockExpressResponse(); await deleteTeam(request, response); expect(response.statusCode).toBe(200); teams = await getConnection().getRepository(Team).find({}); expect(teams.length).toBe(0); const userNoTeam = await getConnection() .getRepository(User) .findOne({ where: { id: addedUser1.id }, relations: ['teams'] }); expect(userNoTeam.teams.length).toBe(0); const badRequest = new MockExpressRequest({ params: {}, }); const badResponse = new MockExpressResponse(); await deleteTeam(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const badRequest2 = new MockExpressRequest({ params: { teamId: 34, }, }); const badResponse2 = new MockExpressResponse(); await deleteTeam(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); }); test('get user teams', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); const teamMember1 = new User(); teamMember1.firstName = 'master'; teamMember1.lastName = 'chief'; teamMember1.email = 'testing1@jest.com'; teamMember1.active = true; const uuid = uuidv4(); teamMember1.uuid = uuid; teamMember1.password = await generateHash('TangoDown123!!!'); const addedUser1 = await getConnection() .getRepository(User) .save(teamMember1); // Team 1 const bravoTeam = new Team(); bravoTeam.name = 'Bravo'; bravoTeam.organization = savedOrg; bravoTeam.id = null; bravoTeam.createdDate = new Date(); bravoTeam.lastUpdatedDate = new Date(); bravoTeam.createdBy = 0; bravoTeam.lastUpdatedBy = 0; bravoTeam.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(bravoTeam); const fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['users'] }); fetchTeam.users.push(addedUser1); await getConnection().getRepository(Team).save(fetchTeam); // Team 2 const alphaTeam = new Team(); alphaTeam.name = 'Alpha'; alphaTeam.organization = savedOrg; alphaTeam.id = null; alphaTeam.createdDate = new Date(); alphaTeam.lastUpdatedDate = new Date(); alphaTeam.createdBy = 0; alphaTeam.lastUpdatedBy = 0; alphaTeam.role = ROLE.TESTER; const savedTeam2 = await getConnection() .getRepository(Team) .save(alphaTeam); const fetchTeam2 = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam2.id }, relations: ['users'] }); fetchTeam2.users.push(addedUser1); await getConnection().getRepository(Team).save(fetchTeam2); const request = new MockExpressRequest({ user: addedUser1.id, }); const response = new MockExpressResponse(); await getMyTeams(request, response); expect(response.statusCode).toBe(200); const userTeams: Team[] = response._getJSON(); expect(userTeams[0].name).toBe(bravoTeam.name); expect(userTeams[1].name).toBe(alphaTeam.name); }); test('add asset to team', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); const assessments: Assessment[] = []; const asset1: Asset = { id: null, name: 'testAsset1', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset1 = await getConnection().getRepository(Asset).save(asset1); const asset2: Asset = { id: null, name: 'testAsset2', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset2 = await getConnection().getRepository(Asset).save(asset2); const asset3: Asset = { id: null, name: 'testAsset3', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset3 = await getConnection().getRepository(Asset).save(asset3); // Team 1 const team1 = new Team(); team1.name = 'Bravo'; team1.organization = savedOrg; team1.id = null; team1.createdDate = new Date(); team1.lastUpdatedDate = new Date(); team1.createdBy = 0; team1.lastUpdatedBy = 0; team1.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(team1); const request = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, savedAsset3.id], teamId: savedTeam.id, }, }); const response = new MockExpressResponse(); await addTeamAsset(request, response); expect(response.statusCode).toBe(200); const fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['assets'] }); expect(fetchTeam.assets.length).toBe(3); const badRequest = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, savedAsset3.id], }, }); const badResponse = new MockExpressResponse(); await addTeamAsset(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const badRequest2 = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, savedAsset3.id], teamId: 5, }, }); const badResponse2 = new MockExpressResponse(); await addTeamAsset(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); const badRequest3 = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, 5], teamId: savedTeam.id, }, }); const badResponse3 = new MockExpressResponse(); await addTeamAsset(badRequest3, badResponse3); expect(badResponse3.statusCode).toBe(401); }); test('remove asset from team', async () => { // create org const newOrg: Organization = { id: null, name: 'Test Org', status: status.active, asset: null, teams: null, }; const savedOrg = await getConnection() .getRepository(Organization) .save(newOrg); const assessments: Assessment[] = []; const asset1: Asset = { id: null, name: 'testAsset1', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset1 = await getConnection().getRepository(Asset).save(asset1); const asset2: Asset = { id: null, name: 'testAsset2', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset2 = await getConnection().getRepository(Asset).save(asset2); const asset3: Asset = { id: null, name: 'testAsset3', status: status.active, organization: savedOrg, assessment: assessments, jira: null, teams: null, }; const savedAsset3 = await getConnection().getRepository(Asset).save(asset3); // Team 1 const team1 = new Team(); team1.name = 'Bravo'; team1.organization = savedOrg; team1.id = null; team1.createdDate = new Date(); team1.lastUpdatedDate = new Date(); team1.createdBy = 0; team1.lastUpdatedBy = 0; team1.role = ROLE.READONLY; const savedTeam = await getConnection().getRepository(Team).save(team1); const fetchTeam = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['assets'] }); fetchTeam.assets.push(savedAsset1, savedAsset2, savedAsset3); await getConnection().getRepository(Team).save(fetchTeam); const fetchTeamWithAssets = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['assets'] }); expect(fetchTeamWithAssets.assets.length).toBe(3); const request = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id], teamId: savedTeam.id, }, }); const response = new MockExpressResponse(); await removeTeamAsset(request, response); const fetchTeamWithRemovedAsset = await getConnection() .getRepository(Team) .findOne({ where: { id: savedTeam.id }, relations: ['assets'] }); expect(fetchTeamWithRemovedAsset.assets.length).toBe(1); const badRequest = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, savedAsset3.id], }, }); const badResponse = new MockExpressResponse(); await removeTeamAsset(badRequest, badResponse); expect(badResponse.statusCode).toBe(400); const badRequest2 = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, savedAsset3.id], teamId: 5, }, }); const badResponse2 = new MockExpressResponse(); await removeTeamAsset(badRequest2, badResponse2); expect(badResponse2.statusCode).toBe(404); const badRequest3 = new MockExpressRequest({ body: { assetIds: [savedAsset1.id, savedAsset2.id, 5], teamId: savedTeam.id, }, }); const badResponse3 = new MockExpressResponse(); await removeTeamAsset(badRequest3, badResponse3); expect(badResponse3.statusCode).toBe(401); }); test('get all teams', async () => { const request = new MockExpressRequest({}); const response = new MockExpressResponse(); await getAllTeams(request, response); const fetchedTeams: Team[] = response._getJSON(); expect(fetchedTeams.length).toBe(0); }); }); ================================================ FILE: src/routes/team.controller.ts ================================================ import { getRepository, In } from 'typeorm'; import { Team } from '../entity/Team'; import { User } from '../entity/User'; import { Response, Request } from 'express'; import { validate } from 'class-validator'; import { UserRequest } from '../interfaces/user-request.interface'; import { Asset } from '../entity/Asset'; import { Organization } from '../entity/Organization'; import { ROLE } from '../enums/roles-enum'; import { AppDataSource } from '../data-source'; export const getAllTeams = async (req: Request, res: Response) => { try { const teamRepository = AppDataSource.getRepository(Team); const teams = await teamRepository .createQueryBuilder('team') .leftJoinAndSelect('team.users', 'users') .leftJoinAndSelect('team.assets', 'assets') .leftJoinAndSelect('team.organization', 'organization') .select([ 'team', 'assets', 'organization', 'users.firstName', 'users.lastName', 'users.title', 'users.id', ]) .getMany(); return res.status(200).json(teams); } catch (error) { console.error('Error getting all teams:', error); return res.status(500).json('An error occurred while fetching teams'); } }; export const fetchAssets = async (assetIds: number[]) => { try { const assetRepository = AppDataSource.getRepository(Asset); const assets = await assetRepository.find({ where: { id: In(assetIds) } }); return assets; } catch (error) { console.error('Error fetching assets:', error); return []; } }; export const fetchUsers = async (userIds: number[]) => { try { const userRepository = AppDataSource.getRepository(User); const users = await userRepository.find({ where: { id: In(userIds), active: true } }); return users; } catch (error) { console.error('Error fetching users:', error); return []; } }; export const fetchUsersAndUpdateTeam = async ( userIds: number[], teamUsers: User[] ) => { try { if (!userIds) { return []; } else { const newUsers = await fetchUsers(userIds); return newUsers; } } catch (error) { console.error('Error updating team users:', error); return teamUsers || []; } }; export const fetchAssetsAndUpdateTeam = async ( assetIds: number[], teamAssets: Asset[] ) => { try { const newAssets = await fetchAssets(assetIds); return newAssets; } catch (error) { console.error('Error updating team assets:', error); return teamAssets || []; } }; export const getTeamById = async (req: UserRequest, res: Response) => { try { const { teamId } = req.params; if (isNaN(+teamId)) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const fetchedTeam = await teamRepository .createQueryBuilder('team') .leftJoinAndSelect('team.users', 'users') .leftJoinAndSelect('team.assets', 'assets') .leftJoinAndSelect('assets.jira', 'jira') .leftJoinAndSelect('team.organization', 'organization') .where('team.id = :teamId', { teamId }) .select([ 'team', 'assets', 'jira.id', 'organization', 'users.firstName', 'users.lastName', 'users.title', 'users.id', ]) .getOne(); if (!fetchedTeam) { return res.status(404).json('Team not found'); } else { return res.status(200).json(fetchedTeam); } } catch (error) { console.error('Error getting team by ID:', error); return res.status(500).json('An error occurred while fetching the team'); } }; export const createTeam = async (req: UserRequest, res: Response) => { try { const { name, organization, role, assetIds, users } = req.body; const newTeam = new Team(); let fetchedOrg: Organization; const organizationRepository = AppDataSource.getRepository(Organization); if (role !== ROLE.ADMIN) { fetchedOrg = await organizationRepository.findOne({ where: { id: organization }, relations: ['teams'] }); if (!fetchedOrg) { return res.status(404).json('Organization not found'); } } else { fetchedOrg = null; } newTeam.id = null; newTeam.name = name; newTeam.organization = fetchedOrg; newTeam.role = role; newTeam.createdBy = +req.user; newTeam.createdDate = new Date(); newTeam.lastUpdatedBy = +req.user; newTeam.lastUpdatedDate = new Date(); if (users && users.length) { newTeam.users = await fetchUsers(users); } if (assetIds && assetIds.length) { newTeam.assets = await fetchAssets(assetIds); } const errors = await validate(newTeam); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Submitted Team is Invalid'); } else { const teamRepository = AppDataSource.getRepository(Team); await teamRepository.save(newTeam); return res.status(200).json('The team has been successfully created'); } } catch (error) { console.error('Error creating team:', error); return res.status(500).json('An error occurred while creating the team'); } }; export const addTeamMember = async (req: UserRequest, res: Response) => { try { const { userIds, teamId } = req.body; // Verify team exists if (!teamId) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const team = await teamRepository.findOne({ where: { id: teamId }, relations: ['users'] }); if (!team) { return res.status(404).json(`A Team with ID ${teamId} does not exist`); } // Verify each user ID links to a valid user team.users = await fetchUsers(userIds); // Save the team once valid users have been pushed to team await teamRepository.save(team); return res.status(200).json('Team membership has been successfully updated'); } catch (error) { console.error('Error adding team member:', error); return res.status(500).json('An error occurred while adding team members'); } }; export const removeTeamMember = async (req: UserRequest, res: Response) => { try { const userIds = req.body.userIds; const teamId = req.body.teamId; // Verify team exists if (!teamId) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const userRepository = AppDataSource.getRepository(User); const team = await teamRepository.findOne({ where: { id: teamId }, relations: ['users'] }); if (!team) { return res.status(404).json(`A Team with ID ${teamId} does not exist`); } // Verify each user ID links to a valid user for (const userId of userIds) { const user = await userRepository.findOne({ where: { id: userId } }); if (!user) { return res.status(404).json(`A User with ID ${userId} does not exist`); } else { team.users = team.users.filter((userIdx) => userIdx.id !== user.id); } } // Save the team once valid users have been pushed to team await teamRepository.save(team); return res.status(200).json('Team membership has been successfully updated'); } catch (error) { console.error('Error removing team member:', error); return res.status(500).json('An error occurred while removing team members'); } }; export const updateTeamInfo = async (req: Request, res: Response) => { try { const { name, role, id, users } = req.body; let organization: Organization = req.body.organization; let assetIds: number[] = req.body.assetIds; if (!(name || organization || role)) { return res.status(400).json('Team is invalid'); } if (!id) { return res.status(400).json('The Team ID is invalid'); } const teamRepository = AppDataSource.getRepository(Team); const organizationRepository = AppDataSource.getRepository(Organization); const team = await teamRepository .createQueryBuilder('team') .leftJoinAndSelect('team.users', 'users') .leftJoinAndSelect('team.assets', 'assets') .leftJoinAndSelect('team.organization', 'organization') .leftJoinAndSelect('organization.asset', 'orgAssets') .where('team.id = :teamId', { teamId: id }) .select(['team', 'users', 'assets', 'organization', 'orgAssets']) .getOne(); if (!team) { return res.status(404).json(`A Team with ID ${id} does not exist`); } // If the incoming organization has changed // Remove all previous asset associations if (role !== ROLE.ADMIN) { organization = await organizationRepository.findOne({ where: { id: +organization }, relations: ['teams'] }); if (!organization) { return res.status(404).json('Organization not found'); } if (team.organization && organization && +organization.id !== +team.organization.id) { for (const orgAsset of team.organization.asset) { assetIds = assetIds.filter((x) => x !== orgAsset.id); } } } else { organization = null; } team.users = await fetchUsersAndUpdateTeam(users, team.users); team.assets = await fetchAssetsAndUpdateTeam(assetIds, team.assets); team.name = name; team.organization = organization; team.role = role; const errors = await validate(team); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Submitted Team is Invalid'); } else { await teamRepository.save(team); return res.status(200).json('Team has been patched successfully'); } } catch (error) { console.error('Error updating team:', error); return res.status(500).json('An error occurred while updating the team'); } }; export const deleteTeam = async (req: Request, res: Response) => { try { const { teamId } = req.params; if (!teamId) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const team = await teamRepository.findOne({ where: { id: +teamId } }); if (!team) { return res.status(404).json(`A Team with ID ${teamId} does not exist`); } await teamRepository.remove(team); return res .status(200) .json(`The Team ${team.name} has been successfully deleted`); } catch (error) { console.error('Error deleting team:', error); return res.status(500).json('An error occurred while deleting the team'); } }; export const getMyTeams = async (req: UserRequest, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.user }, relations: ['teams'] }); if (!user) { return res.status(404).json('User not found'); } return res.status(200).json(user.teams); } catch (error) { console.error('Error getting my teams:', error); return res.status(500).json('An error occurred while fetching your teams'); } }; export const addTeamAsset = async (req: Request, res: Response) => { try { const { assetIds, teamId } = req.body; if (!teamId) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const assetRepository = AppDataSource.getRepository(Asset); const organizationRepository = AppDataSource.getRepository(Organization); const team = await teamRepository.findOne({ where: { id: teamId }, relations: ['assets', 'organization'] }); if (!team) { return res.status(404).json(`A Team with ID ${teamId} does not exist`); } const org = await organizationRepository.findOne({ where: { id: team.organization.id }, relations: ['asset'] }); const orgAssets = org.asset.map((asset) => asset.id); // Verify each asset ID links to a valid asset for (const assetId of assetIds) { // Verify we are only associating assets of that organization if (!orgAssets.includes(assetId)) { return res .status(401) .json(`The Asset with ID ${assetId} is unauthorized`); } const asset = await assetRepository.findOne({ where: { id: assetId } }); if (!asset) { return res.status(404).json(`Asset with ID ${assetId} not found`); } team.assets.push(asset); } // Save the team once valid assets have been pushed to team await teamRepository.save(team); return res.status(200).json('Team Assets has been successfully updated'); } catch (error) { console.error('Error adding team asset:', error); return res.status(500).json('An error occurred while adding team assets'); } }; export const removeTeamAsset = async (req: Request, res: Response) => { try { const { assetIds, teamId } = req.body; if (!teamId) { return res.status(400).json('Invalid Team ID'); } const teamRepository = AppDataSource.getRepository(Team); const assetRepository = AppDataSource.getRepository(Asset); const organizationRepository = AppDataSource.getRepository(Organization); const team = await teamRepository.findOne({ where: { id: teamId }, relations: ['assets', 'organization'] }); if (!team) { return res.status(404).json(`A Team with ID ${teamId} does not exist`); } const org = await organizationRepository.findOne({ where: { id: team.organization.id }, relations: ['asset'] }); const orgAssets = org.asset.map((asset) => asset.id); // Verify each asset ID links to a valid asset for (const assetId of assetIds) { // Verify we are only associating assets of that organization if (!orgAssets.includes(assetId)) { return res .status(401) .json(`The Asset with ID ${assetId} is unauthorized`); } const asset = await assetRepository.findOne({ where: { id: assetId } }); if (!asset) { return res.status(404).json(`Asset with ID ${assetId} not found`); } team.assets = team.assets.filter((assetIdx) => assetIdx.id !== asset.id); } // Save the team once assets have been removed await teamRepository.save(team); return res.status(200).json('Team Assets has been successfully updated'); } catch (error) { console.error('Error removing team asset:', error); return res.status(500).json('An error occurred while removing team assets'); } }; ================================================ FILE: src/routes/user.controller.spec.ts ================================================ import { createConnection, getConnection } from 'typeorm'; import * as userController from './user.controller'; import { User } from '../entity/User'; import { v4 as uuidv4 } from 'uuid'; import { generateHash } from '../utilities/password.utility'; import { Config } from '../entity/Config'; import MockExpressResponse = require('mock-express-response'); import MockExpressRequest = require('mock-express-request'); import { Team } from '../entity/Team'; import { Organization } from '../entity/Organization'; import { Asset } from '../entity/Asset'; import { Assessment } from '../entity/Assessment'; import { Jira } from '../entity/Jira'; import { ProblemLocation } from '../entity/ProblemLocation'; import { ReportAudit } from '../entity/ReportAudit'; import { Resource } from '../entity/Resource'; import { Vulnerability } from '../entity/Vulnerability'; import { File } from '../entity/File'; import { ApiKey } from '../entity/ApiKey'; describe('User Controller', () => { beforeEach(() => { return createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Config, User, Team, Organization, Asset, Assessment, Vulnerability, ProblemLocation, ReportAudit, Resource, Jira, File, ApiKey, ], synchronize: true, logging: false, }); }); afterEach(() => { const conn = getConnection(); return conn.close(); }); test('invite user fail', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = null; config.id = 1; await getConnection().getRepository(Config).insert(config); const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'testing@jest.com', }; await userController.invite(req, res); expect(res.statusCode).toBe(400); }); test('invite user failure missing email', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = null; config.id = 1; await getConnection().getRepository(Config).insert(config); const req = new MockExpressRequest(); req.body = {}; const res = new MockExpressResponse(); await userController.invite(req, res); expect(res.statusCode).toBe(400); }); test('invite user failure user already exists', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = null; config.id = 1; await getConnection().getRepository(Config).insert(config); const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'testing@jest.com', }; const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; await getConnection().getRepository(User).insert(existUser); await userController.invite(req, res); expect(res.statusCode).toBe(400); }); test('register user failure missing first name', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'testing@jest.com', title: 'Spartan 117', lastName: 'Chief', password: 'notSecure123', }; await userController.register(req, res); expect(res.statusCode).toBe(400); }); test('register user failure missing last name', async () => { const req = new MockExpressRequest(); req.body = { email: 'testing@jest.com', title: 'Spartan 117', firstName: 'Master', password: 'notSecure123', }; const res = new MockExpressResponse(); await userController.register(req, res); expect(res.statusCode).toBe(400); }); test('register user failure missing title', async () => { const req = new MockExpressRequest(); req.body = { email: 'testing@jest.com', lastName: 'Chief', firstName: 'Master', password: 'notSecure123', }; const res = new MockExpressResponse(); await userController.register(req, res); expect(res.statusCode).toBe(400); }); test('register user failure passwords do not match', async () => { const req = new MockExpressRequest(); req.body = { email: 'testing@jest.com', lastName: 'Chief', firstName: 'Master', password: 'notSecure123', confirmPassword: 'notSecureAbc', title: 'Spartan 117', }; const res = new MockExpressResponse(); await userController.register(req, res); expect(res.statusCode).toBe(400); }); test('register user failure password validation', async () => { const req = new MockExpressRequest(); req.body = { email: 'testing@jest.com', lastName: 'Chief', firstName: 'Master', password: '123', confirmPassword: '123', title: 'Spartan 117', }; const res = new MockExpressResponse(); await userController.register(req, res); expect(res.statusCode).toBe(400); }); test('register user success', async () => { const config = new Config(); config.fromEmail = 'testingDude@jest.com'; config.companyName = 'Test'; config.fromEmailPassword = null; config.id = 1; const user = new User(); user.active = false; user.uuid = uuidv4(); user.email = 'testing@jest.com'; user.newEmail = null; await getConnection().getRepository(User).save(user); await getConnection().getRepository(Config).insert(config); const req = new MockExpressRequest(); const invReq = new MockExpressRequest(); invReq.body = { email: 'testing@jest.com', }; const res = new MockExpressResponse(); await userController.invite(invReq, res); const invUser = await getConnection() .getRepository(User) .find({ where: { email: 'testing@jest.com' } }); req.body = { email: invUser[0].email, lastName: 'Chief', firstName: 'Master', password: '&3x1GqpeFO61*HJ', confirmPassword: '&3x1GqpeFO61*HJ', title: 'Spartan 117', uuid: invUser[0].uuid, }; const res2 = new MockExpressResponse(); await userController.register(req, res2); expect(res2.statusCode).toBe(200); }); test('verify user failure no uuid', async () => { const mRequest = () => { const req = { params: {}, user: Function, }; req.user = jest.fn().mockReturnValue(req); return req; }; const vReq = new MockExpressRequest(); const res = new MockExpressResponse(); vReq.params = {}; await userController.verify(vReq, res); expect(res.statusCode).toBe(400); }); test('verify user success', async () => { const res = new MockExpressResponse(); const mRequest = () => { const r = { params: {}, user: Function, }; r.user = jest.fn().mockReturnValue(r); return r; }; const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = false; const uuid = uuidv4(); existUser.uuid = uuid; await getConnection().getRepository(User).insert(existUser); const verifyReq = new MockExpressRequest(); verifyReq.params = { uuid, }; await userController.verify(verifyReq, res); expect(res.statusCode).toBe(200); }); test('verify user failure user does not exist', async () => { const res = new MockExpressResponse(); const mRequest = () => { const r = { params: {}, user: Function, }; r.user = jest.fn().mockReturnValue(r); return r; }; const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = false; const uuid = uuidv4(); existUser.uuid = uuid; await getConnection().getRepository(User).insert(existUser); const verifyReq = new MockExpressRequest(); const uuid2 = uuidv4(); verifyReq.params = { uuid: uuid2, }; await userController.verify(verifyReq, res); expect(res.statusCode).toBe(400); }); test('Update user password failure passwords do not match', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { oldPassword: 'fakePassword', newPassword: 'fakePassword2', confirmNewPassword: 'fakePasswordDifferent', }; await userController.updateUserPassword(req, res); expect(res.statusCode).toBe(400); }); test('Update user password failure same password', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { oldPassword: 'fakePassword', newPassword: 'fakePassword', confirmNewPassword: 'fakePassword', }; await userController.updateUserPassword(req, res); expect(res.statusCode).toBe(400); }); test('Update user password failure password validation', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { oldPassword: '123', newPassword: '234', confirmNewPassword: '234', }; await userController.updateUserPassword(req, res); expect(res.statusCode).toBe(400); }); test('Update user password success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { oldPassword: 'TangoDown123!!!', newPassword: '9z4O4^HSvHkt3iU', confirmNewPassword: '9z4O4^HSvHkt3iU', }; const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const userr = await getConnection().getRepository(User).insert(existUser); req.user = userr.identifiers[0].id; await userController.updateUserPassword(req, res); expect(res.statusCode).toBe(200); }); test('Update user failure user does not exist', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { oldPassword: 'TangoDown123!!!', newPassword: '9z4O4^HSvHkt3iU', confirmNewPassword: '9z4O4^HSvHkt3iU', }; const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'testing@jest.com'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const userr = await getConnection().getRepository(User).insert(existUser); userr.identifiers[0].id = 117; const x: any = 2; req.user = x; await userController.updateUserPassword(req, res); expect(res.statusCode).toBe(400); }); test('patch user success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { firstName: 'Master', lastName: 'Chief', title: 'Spartan 117', }; const existUser = new User(); existUser.firstName = 'Cortana'; existUser.lastName = 'AI'; existUser.email = 'testing@jest.com'; existUser.title = 'A.I.'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); existUser.newEmail = null; const userr = await getConnection().getRepository(User).insert(existUser); req.user = userr.identifiers[0].id; await userController.patch(req, res); expect(res.statusCode).toBe(200); }); test('get user failure user does not exist', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const existUser = new User(); existUser.firstName = 'Cortana'; existUser.lastName = 'AI'; existUser.email = 'testing@jest.com'; existUser.title = 'A.I.'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); await getConnection().getRepository(User).insert(existUser); const x: any = 2; req.user = x; await userController.getUser(req, res); expect(res.statusCode).toBe(404); }); test('get user success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const existUser = new User(); existUser.firstName = 'Cortana'; existUser.lastName = 'AI'; existUser.email = 'testing@jest.com'; existUser.title = 'A.I.'; existUser.active = true; const uuid = uuidv4(); existUser.uuid = uuid; existUser.password = await generateHash('TangoDown123!!!'); const userr = await getConnection().getRepository(User).insert(existUser); req.user = userr.identifiers[0].id; await userController.getUser(req, res); expect(res.statusCode).toBe(200); }); test('get users success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); await userController.getUsers(req, res); expect(res.statusCode).toBe(200); }); test('get users by id success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.title = 'A.I.'; user1.active = true; const uuid = uuidv4(); user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const user2 = new User(); user2.firstName = 'Cortana'; user2.lastName = 'AI'; user2.email = 'testing2@jest.com'; user2.title = 'A.I.'; user2.active = true; const uuid2 = uuidv4(); user2.uuid = uuid2; user2.password = await generateHash('TangoDown123!!!'); const usrIdAry: number[] = []; const insUser1 = await getConnection().getRepository(User).insert(user1); usrIdAry.push(insUser1.identifiers[0].id); const insUser2 = await getConnection().getRepository(User).insert(user2); usrIdAry.push(insUser2.identifiers[0].id); const retUserAry = await userController.getUsersById(usrIdAry); expect(retUserAry).toHaveLength(2); }); test('Update user email failure emails do not match', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: '123', newEmail: '234', }; await userController.updateUserEmail(req, res); expect(res.statusCode).toBe(400); }); test('Update user email failure in progress email update request', async () => { const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = 'alreadySent@lol.com'; user1.title = 'A.I.'; user1.active = true; const uuid = uuidv4(); user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'test@jest.com', newEmail: 'test@jest.com', }; req.user = 1; await userController.updateUserEmail(req, res); expect(res.statusCode).toBe(400); }); test('Update user email failure email already exists', async () => { const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = ''; user1.title = 'A.I.'; user1.active = true; const uuid = uuidv4(); user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const user2 = new User(); user2.firstName = 'Cortana'; user2.lastName = 'AI'; user2.email = 'testing2@jest.com'; user2.title = 'A.I.'; user2.active = true; const uuid2 = uuidv4(); user2.uuid = uuid2; user2.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); const insUser2 = await getConnection().getRepository(User).insert(user2); const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'testing2@jest.com', newEmail: 'testing2@jest.com', }; req.user = 1; await userController.updateUserEmail(req, res); expect(res.statusCode).toBe(400); }); test('Update user email failure user entity email validation', async () => { const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = ''; user1.title = 'A.I.'; user1.active = true; const uuid = uuidv4(); user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'test@', newEmail: 'test@', }; req.user = 1; await userController.updateUserEmail(req, res); expect(res.statusCode).toBe(400); }); test('revoke email request success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.user = 1; const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = 'newEmailToBeRevoked@jest.com'; user1.title = 'A.I.'; user1.active = true; const uuid = uuidv4(); user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); await userController.revokeEmailRequest(req, res); const checkUser = await getConnection() .getRepository(User) .findOne(req.user); expect(checkUser.uuid).toBeNull(); expect(checkUser.newEmail).toBeNull(); expect(res.statusCode).toBe(200); }); test('revoke email request failure user does not exist', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.user = 1; await userController.revokeEmailRequest(req, res); expect(res.statusCode).toBe(404); }); test('validate email request failure missing uuid or password', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { password: 'test', uuid: '', }; await userController.validateEmailRequest(req, res); expect(res.statusCode).toBe(400); const req2 = new MockExpressRequest(); const res2 = new MockExpressResponse(); req2.body = { password: '', uuid: 'test', }; await userController.validateEmailRequest(req2, res2); expect(res2.statusCode).toBe(400); }); test('validate email request failure user does not exist', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const uuid = uuidv4(); req.body = { password: 'test', uuid, }; await userController.validateEmailRequest(req, res); expect(res.statusCode).toBe(404); }); test('validate email request failure invalid password', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const uuid = uuidv4(); req.body = { password: 'test', uuid, }; req.user = 1; const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = 'newEmailToBeRevoked@jest.com'; user1.title = 'A.I.'; user1.active = true; user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); await userController.validateEmailRequest(req, res); expect(res.statusCode).toBe(400); }); test('validate email request success', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); const uuid = uuidv4(); req.body = { password: 'TangoDown123!!!', uuid, }; req.user = 1; const user1 = new User(); user1.firstName = 'Cortana'; user1.lastName = 'AI'; user1.email = 'testing1@jest.com'; user1.newEmail = 'newEmail@jest.com'; user1.title = 'A.I.'; user1.active = true; user1.uuid = uuid; user1.password = await generateHash('TangoDown123!!!'); const insUser1 = await getConnection().getRepository(User).insert(user1); await userController.validateEmailRequest(req, res); const checkUser = await getConnection() .getRepository(User) .findOne(req.user); expect(checkUser.email).toBe('newEmail@jest.com'); expect(checkUser.newEmail).toBeNull(); expect(checkUser.uuid).toBeNull(); expect(res.statusCode).toBe(200); }); test('create user', async () => { const req = new MockExpressRequest(); const res = new MockExpressResponse(); req.body = { email: 'createUser1@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', }; await userController.create(req, res); expect(res.statusCode).toBe(400); const req2 = new MockExpressRequest(); const res2 = new MockExpressResponse(); req2.body = { email: 'createUser1@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', password: 'as;dfk23a234adsf', confirmPassword: '123', }; await userController.create(req2, res2); expect(res2.statusCode).toBe(400); const req3 = new MockExpressRequest(); const res3 = new MockExpressResponse(); req3.body = { email: 'createUser1@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', password: 'weak', confirmPassword: 'weak', }; await userController.create(req3, res3); expect(res3.statusCode).toBe(400); const req4 = new MockExpressRequest(); const res4 = new MockExpressResponse(); req4.body = { email: 'createUser1@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', password: '&3x1GqpeFO61*HJ', confirmPassword: '&3x1GqpeFO61*HJ', }; await userController.create(req4, res4); expect(res4.statusCode).toBe(200); const existUser = new User(); existUser.firstName = 'master'; existUser.lastName = 'chief'; existUser.email = 'createUserExist@jest.com'; existUser.active = true; await getConnection().getRepository(User).save(existUser); const req5 = new MockExpressRequest(); const res5 = new MockExpressResponse(); req5.body = { email: 'createUserExist@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', password: '&3x1GqpeFO61*HJ', confirmPassword: '&3x1GqpeFO61*HJ', }; await userController.create(req5, res5); expect(res5.statusCode).toBe(400); const req6 = new MockExpressRequest(); const res6 = new MockExpressResponse(); req6.body = { email: 'createUserExist@jest.com', firstName: 'John', lastName: 'Doe', title: 'Senior Dairy Farmer', password: '&3x1GqpeFO61*HJ', confirmPassword: '&3x1GqpeFO61*HJ', }; await userController.create(req6, res6); expect(res6.statusCode).toBe(400); }); }); ================================================ FILE: src/routes/user.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { AppDataSource } from '../data-source'; import { User } from '../entity/User'; import { Response, Request } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { validate } from 'class-validator'; import { passwordRequirement } from '../enums/message-enum'; import { compare, generateHash, passwordSchema, updatePassword, } from '../utilities/password.utility'; import * as emailService from '../services/email.service'; import { Config } from '../entity/Config'; import { ROLE } from '../enums/roles-enum'; import { In } from 'typeorm'; /** * @description Register user * @param {UserRequest} req * @param {Response} res * @returns success message */ export const register = async (req: Request, res: Response) => { try { const { firstName, lastName, title, password, confirmPassword, uuid, } = req.body; if (!firstName || !lastName || !title) { return res.status(400).json('Invalid registration form'); } if (password !== confirmPassword) { return res.status(400).json('Passwords do not match'); } if (!passwordSchema.validate(password)) { return res.status(400).json(passwordRequirement); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository .createQueryBuilder('user') .where('user.uuid = :uuid', { uuid, }) .getOne(); if (user) { user.password = await generateHash(password); user.uuid = null; user.active = true; user.firstName = firstName; user.lastName = lastName; user.title = title; const errors = await validate(user, { skipMissingProperties: true }); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Invalid registration form'); } else { await userRepository.save(user); return res.status(200).json('Registration Complete'); } } else { return res .status(400) .json( 'Unable to register user password at this time. Please contact an administrator for assistance.' ); } } catch (error) { console.error('Error during registration:', error); return res.status(500).json('An error occurred during registration'); } }; /** * @description Invite user * @param {UserRequest} req * @param {Response} res * @returns success message */ export const invite = async (req: Request, res: Response) => { try { const configRepository = AppDataSource.getRepository(Config); const config = await configRepository.findOne({ where: { id: 1 } }); if (!config || !config.fromEmail || !config.fromEmailPassword) { return res .status(400) .json( 'Failed to invite user. Please set the email configuration in the Settings menu option.' ); } else { const { email } = req.body; if (!email) { return res.status(400).json('Email is invalid'); } const userRepository = AppDataSource.getRepository(User); const existUser = await userRepository.find({ where: { email } }); if (existUser.length) { return res .status(400) .json('A user associated to that email has already been invited'); } const user = new User(); user.active = false; user.uuid = uuidv4(); user.email = email; await userRepository.save(user); emailService.sendInvitationEmail(user.uuid, user.email); return res.status(200).json('User invited successfully'); } } catch (error) { console.error('Error inviting user:', error); return res.status(500).json('An error occurred while inviting the user'); } }; /** * @description Create user * @param {UserRequest} req * @param {Response} res * @returns success message */ export const create = async (req: Request, res: Response) => { try { const { email, firstName, lastName, title, password, confirmPassword, } = req.body; if ( !email || !firstName || !lastName || !title || !password || !confirmPassword ) { return res.status(400).json('Invalid user form'); } if (password !== confirmPassword) { return res.status(400).json('Passwords do not match'); } if (!passwordSchema.validate(password)) { return res.status(400).json(passwordRequirement); } const userRepository = AppDataSource.getRepository(User); const existUser = await userRepository .findOne({ where: { email } }); if (existUser) { return res .status(400) .json('A user associated to that email has already been created'); } const user = new User(); user.active = true; user.uuid = uuidv4(); user.email = email; user.newEmail = email; user.firstName = firstName; user.lastName = lastName; user.title = title; user.password = await generateHash(password); await userRepository.save(user); return res.status(200).json('User created successfully'); } catch (error) { console.error('Error creating user:', error); return res.status(500).json('An error occurred while creating the user'); } }; /** * @description Verifies user by comparing UUID * @param {UserRequest} req * @param {Response} res * @returns Success message */ export const verify = async (req: Request, res: Response) => { try { if (req.params.uuid) { const userRepository = AppDataSource.getRepository(User); const user = await userRepository .findOne({ where: { uuid: req.params.uuid } }); if (user) { user.active = true; user.uuid = null; await userRepository.save(user); return res.status(200).json('Email verification successful'); } else { return res .status(400) .json('Email verification failed. User does not exist.'); } } else { return res.status(400).json('UUID is undefined'); } } catch (error) { console.error('Error during verification:', error); return res.status(500).json('An error occurred during verification'); } }; /** * @description Updates user password * @param {UserRequest} req * @param {Response} res * @returns Success message */ export const updateUserPassword = async (req: UserRequest, res: Response) => { try { const { oldPassword, newPassword, confirmNewPassword } = req.body; const currentPassword = oldPassword; if (newPassword !== confirmNewPassword) { return res.status(400).json('Passwords do not match'); } if (newPassword === currentPassword) { return res .status(400) .json('New password can not be the same as the old password'); } if (!passwordSchema.validate(newPassword)) { return res.status(400).json(passwordRequirement); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.user } }); if (user) { const hashedUserPassword = user.password; try { user.password = await updatePassword( hashedUserPassword, currentPassword, newPassword ); } catch (err) { return res.status(400).json(err); } await userRepository.save(user); return res.status(200).json('Password updated successfully'); } else { return res .status(400) .json( 'Unable to update user password at this time. Please contact an administrator for assistance.' ); } } catch (error) { console.error('Error updating password:', error); return res.status(500).json('An error occurred while updating the password'); } }; /** * @description Patch user * @param {UserRequest} req * @param {Response} res * @returns Success message */ export const patch = async (req: UserRequest, res: Response) => { try { const { firstName, lastName, title } = req.body; const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.user } }); if (!user) { return res.status(404).json('User not found'); } if (firstName) { user.firstName = firstName; } if (lastName) { user.lastName = lastName; } if (title) { user.title = title; } user.uuid = null; const errors = await validate(user, { skipMissingProperties: true }); if (errors.length > 0) { return res.status(400).json(errors); } else { await userRepository.save(user); return res.status(200).json('User patched successfully'); } } catch (error) { console.error('Error updating user:', error); return res.status(500).json('An error occurred while updating the user'); } }; /** * @description Get user * @param {UserRequest} req * @param {Response} res * @returns User */ export const getUser = async (req: UserRequest, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository .findOne({ where: { id: +req.user }, relations: ['teams'] }); if (!user) return res.status(404).json('User not found'); // Don't return sensitive data const userResponse = { ...user }; delete userResponse.active; delete userResponse.password; delete userResponse.uuid; delete userResponse.id; return res.status(200).json(userResponse); } catch (error) { console.error('Error fetching user:', error); return res.status(500).json('An error occurred while fetching the user'); } }; /** * @description Get Users * @param {UserRequest} req * @param {Response} res * @returns user array */ export const getUsers = async (req: Request, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const users = await userRepository .createQueryBuilder('user') .where('user.active = true') .select(['user.id', 'user.firstName', 'user.lastName', 'user.title']) .getMany(); return res.status(200).json(users); } catch (error) { console.error('Error fetching users:', error); return res.status(500).json('An error occurred while fetching users'); } }; /** * @description Get Testers * @param {UserRequest} req * @param {Response} res * @returns user array */ export const getTesters = async (req: UserRequest, res: Response) => { try { if (!req.params.orgId) { return res.status(400).json('Invalid Organization ID'); } if (isNaN(+req.params.orgId)) { return res.status(400).json('Invalid Organization ID'); } // Optional check for org access // if (!req.userOrgs.includes(+req.params.orgId)) { // return res.status(404).json('Testers not found'); // } const userRepository = AppDataSource.getRepository(User); const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.teams', 'teams') .leftJoinAndSelect('teams.organization', 'organization') .where('user.active = true') .andWhere('teams.role != :role', { role: ROLE.READONLY }) .andWhere('teams.organization.id = :orgId', { orgId: +req.params.orgId }) .select(['user.id', 'user.firstName', 'user.lastName', 'user.title']) .getMany(); return res.status(200).json(users); } catch (error) { console.error('Error fetching testers:', error); return res.status(500).json('An error occurred while fetching testers'); } }; /** * @description Get all active/inactive users * @param {UserRequest} req * @param {Response} res * @returns user array */ export const getAllUsers = async (req: Request, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.teams', 'teams') .select([ 'user.id', 'user.firstName', 'user.lastName', 'user.title', 'user.active', 'user.email', 'teams.name', ]) .getMany(); return res.status(200).json(users); } catch (error) { console.error('Error fetching all users:', error); return res.status(500).json('An error occurred while fetching all users'); } }; /** * @description Get user IDs and validate users * @param {number[]} userIds * @returns User array */ export const getUsersById = async (userIds: number[]) => { try { const userRepository = AppDataSource.getRepository(User); const users = await userRepository.find({ where: { id: In(userIds) } }); return users; } catch (error) { console.error('Error fetching users by ID:', error); return []; } }; /** * @description Send email update request * @param {UserRequest} req * @param {Response} res * @returns string */ export const updateUserEmail = async (req: UserRequest, res: Response) => { try { const email = req.body.email; const newEmail = req.body.newEmail; if (email !== newEmail) { return res .status(400) .json('The new email address and confirmation email address must match'); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.user } }); if (!user) { return res.status(404).json('User not found'); } if (user.newEmail) { return res .status(400) .json( 'An email update request is already in progress. Please revoke this current request and try again.' ); } // Check if email is already taken const existingUsers = await userRepository.find({ select: ['email'] }); const existingEmails = existingUsers.map((x) => x.email); if (existingEmails.includes(email)) { return res.status(400).json('Email is already taken'); } user.uuid = uuidv4(); user.newEmail = email; const errors = await validate(user); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).json('Email is invalid'); } else { await userRepository.save(user); emailService.sendUpdateUserEmail(user, (err, info) => { if (err) { return res .status(400) .json( 'There was a problem updating your email address. Please contact an administrator for assistance.' ); } else { return res .status(200) .json(`A confirmation email has been sent to ${user.newEmail}`); } }); } } catch (error) { console.error('Error updating email:', error); return res.status(500).json('An error occurred while updating the email'); } }; /** * @description Revoke current email update request * @param {UserRequest} req * @param {Response} res * @returns string */ export const revokeEmailRequest = async (req: UserRequest, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.user } }); if (!user || !user.newEmail) { return res .status(404) .json('An email update request for this user does not exist'); } user.newEmail = null; user.uuid = null; await userRepository.save(user); return res .status(200) .json('The email update request has been successfully revoked'); } catch (error) { console.error('Error revoking email request:', error); return res.status(500).json('An error occurred while revoking the email request'); } }; /** * @description Validate current email update request * @param {UserRequest} req * @param {Response} res * @returns string */ export const validateEmailRequest = async (req: UserRequest, res: Response) => { try { if (!req.body.password || !req.body.uuid) { return res.status(400).json('The password or UUID is missing'); } const userRepository = AppDataSource.getRepository(User); const user = await userRepository .findOne({ where: { uuid: req.body.uuid } }); if (!user) { return res .status(404) .json('A user associated with this UUID does not exist'); } const valid = await compare(req.body.password, user.password); if (!valid) { return res.status(400).json('The password is incorrect'); } else { user.email = user.newEmail; user.uuid = null; user.newEmail = null; await userRepository.save(user); return res.status(200).json('Your email has been successfully updated'); } } catch (error) { console.error('Error validating email request:', error); return res.status(500).json('An error occurred while validating the email request'); } }; /** * @description Activate user * @param {Request} req * @param {Response} res * @returns Success message */ export const activateUser = async (req: Request, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.params.id } }); if (!user) { return res.status(404).json('User not found'); } user.active = true; await userRepository.save(user); return res.status(200).json('User activated successfully'); } catch (error) { console.error('Error activating user:', error); return res.status(500).json('An error occurred while activating the user'); } }; /** * @description Deactivate user * @param {Request} req * @param {Response} res * @returns Success message */ export const deactivateUser = async (req: Request, res: Response) => { try { const userRepository = AppDataSource.getRepository(User); const user = await userRepository.findOne({ where: { id: +req.params.id } }); if (!user) { return res.status(404).json('User not found'); } user.active = false; await userRepository.save(user); return res.status(200).json('User deactivated successfully'); } catch (error) { console.error('Error deactivating user:', error); return res.status(500).json('An error occurred while deactivating the user'); } }; ================================================ FILE: src/routes/vulnerability.controller.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { Response } from 'express'; import { AppDataSource } from '../data-source'; import { Assessment } from '../entity/Assessment'; import { validate } from 'class-validator'; import { Vulnerability } from '../entity/Vulnerability'; import { File } from '../entity/File'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import { exportToJiraIssue } from '../utilities/jira.utility'; import { JiraInit } from '../interfaces/jira/jira-init.interface'; import { Jira } from '../entity/Jira'; import { hasAssetReadAccess, hasAssetWriteAccess, } from '../utilities/role.utility'; const fileUploadController = require('../routes/file-upload.controller'); /** * @description get vulnerability by ID * @param {UserRequest} req * @param {Response} res contains JSON object with the organization data * @returns vulnerability data */ export const getVulnById = async (req: UserRequest, res: Response) => { try { if (!req.params.vulnId) { return res.status(400).send('Invalid Vulnerability Request'); } if (isNaN(+req.params.vulnId)) { return res.status(400).send('Invalid Vulnerability ID'); } const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const assessmentRepository = AppDataSource.getRepository(Assessment); const jiraRepository = AppDataSource.getRepository(Jira); // Get vulnerability with relations const vuln = await vulnerabilityRepository.findOne({ where: { id: +req.params.vulnId }, relations: ['screenshots', 'problemLocations', 'resources', 'assessment'] }); if (!vuln) { return res.status(404).send('Vulnerability does not exist.'); } const assessment = await assessmentRepository.findOne({ where: { id: vuln.assessment.id }, relations: ['asset', 'asset.organization'] }); const hasReadAccess = await hasAssetReadAccess(req, assessment.asset.id); if (!hasReadAccess) { return res.status(404).json('Vulnerability not found'); } const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); const jira = await jiraRepository.findOne({ where: { asset: { id: assessment.asset.id } } }); const jiraHost = jira ? jira.host : null; return res .status(200) .json({ vulnerability: vuln, jiraHost, readOnly: !hasAccess }); } catch (error) { console.error('Error fetching vulnerability:', error); return res.status(500).json('An error occurred while fetching the vulnerability'); } }; /** * @description Delete vulnerability by ID * @param {UserRequest} req vulnID is required * @param {Response} res contains JSON object with the success/fail * @returns success/error message */ export const deleteVulnById = async (req: UserRequest, res: Response) => { try { if (!req.params.vulnId) { return res.status(400).send('Invalid vulnerability request'); } if (isNaN(+req.params.vulnId)) { return res.status(400).send('Invalid vulnerability ID'); } const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const assessmentRepository = AppDataSource.getRepository(Assessment); const vuln = await vulnerabilityRepository.findOne({ where: { id: +req.params.vulnId }, relations: ['assessment'] }); if (!vuln) { return res.status(404).send('Vulnerability does not exist.'); } const assessment = await assessmentRepository.findOne({ where: { id: vuln.assessment.id }, relations: ['asset'] }); const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } await vulnerabilityRepository.remove(vuln); return res .status(200) .json(`Vulnerability #${vuln.id}: "${vuln.name}" successfully deleted`); } catch (error) { console.error('Error deleting vulnerability:', error); return res.status(500).json('An error occurred while deleting the vulnerability'); } }; /** * @description Update vulnerability by ID * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const patchVulnById = async (req: UserRequest, res: Response) => { try { req = await fileUploadController.uploadFileArray(req, res); if (isNaN(+req.body.assessment) || !req.body.assessment) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const fileRepository = AppDataSource.getRepository(File); const problemLocationRepository = AppDataSource.getRepository(ProblemLocation); const resourceRepository = AppDataSource.getRepository(Resource); const assessment = await assessmentRepository.findOne({ where: { id: +req.body.assessment }, relations: ['asset'] }); if (!assessment) { return res.status(404).json('Assessment does not exist'); } const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } if (isNaN(+req.params.vulnId)) { return res.status(400).json('Vulnerability ID is invalid'); } const vulnerability = await vulnerabilityRepository.findOne({ where: { id: +req.params.vulnId } }); if (!vulnerability) { return res.status(404).json('Vulnerability does not exist'); } vulnerability.id = +req.params.vulnId; vulnerability.impact = req.body.impact; vulnerability.likelihood = req.body.likelihood; vulnerability.risk = req.body.risk; vulnerability.status = req.body.status; vulnerability.description = req.body.description; vulnerability.remediation = req.body.remediation; vulnerability.jiraId = req.body.jiraId; vulnerability.cvssScore = req.body.cvssScore; vulnerability.cvssUrl = req.body.cvssUrl; vulnerability.detailedInfo = req.body.detailedInfo; vulnerability.assessment = assessment; vulnerability.name = req.body.name; vulnerability.systemic = req.body.systemic; const errors = await validate(vulnerability); if (errors.length > 0) { return res.status(400).send('Vulnerability form validation failed'); } await vulnerabilityRepository.save(vulnerability); // Remove deleted files if (req.body.screenshotsToDelete) { try { const existingScreenshots = await fileRepository.find({ where: { vulnerability: { id: vulnerability.id } } }); const existingScreenshotIds = existingScreenshots.map( (screenshot) => screenshot.id ); let screenshotsToDelete = JSON.parse(req.body.screenshotsToDelete); // We only want to remove the files associated to the vulnerability screenshotsToDelete = existingScreenshotIds.filter((value) => screenshotsToDelete.includes(value) ); for (const screenshotId of screenshotsToDelete) { await fileRepository.delete(screenshotId); } } catch (error) { console.error('Error processing deleted screenshots:', error); } } // Save new screenshots if (req.files && req.files.length > 0) { await saveScreenshots(req.files, vulnerability); } // Process problem locations if (req.body.problemLocations && req.body.problemLocations.length) { try { const clientProdLocs = JSON.parse(req.body.problemLocations); const clientProdLocsIds = clientProdLocs.map((value) => value.id).filter(id => id); const existingProbLocs = await problemLocationRepository.find({ where: { vulnerability: { id: vulnerability.id } } }); const existingProbLocIds = existingProbLocs.map((probLoc) => probLoc.id); const prodLocsToDelete = existingProbLocIds.filter( (value) => !clientProdLocsIds.includes(value) ); for (const probLoc of prodLocsToDelete) { await problemLocationRepository.delete(probLoc); } await saveProblemLocations(clientProdLocs, vulnerability); } catch (error) { console.error('Error processing problem locations:', error); } } // Process resources if (req.body.resources && req.body.resources.length) { try { const clientResources = JSON.parse(req.body.resources); const clientResourceIds = clientResources.map((value) => value.id).filter(id => id); const existingResources = await resourceRepository.find({ where: { vulnerability: { id: vulnerability.id } } }); const existingResourceIds = existingResources.map( (resource) => resource.id ); const resourcesToDelete = existingResourceIds.filter( (value) => !clientResourceIds.includes(value) ); for (const resource of resourcesToDelete) { await resourceRepository.delete(resource); } await saveResources(clientResources, vulnerability); } catch (error) { console.error('Error processing resources:', error); } } return res.status(200).json('Vulnerability patched successfully'); } catch (error) { console.error('Error updating vulnerability:', error); return res.status(500).json('An error occurred while updating the vulnerability'); } }; /** * @description Create vulnerability * @param {UserRequest} req * @param {Response} res * @returns success/error message */ export const createVuln = async (req: UserRequest, res: Response) => { try { req = await fileUploadController.uploadFileArray(req, res); if (isNaN(+req.body.assessment) || !req.body.assessment) { return res.status(400).json('Invalid Assessment ID'); } const assessmentRepository = AppDataSource.getRepository(Assessment); const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const assessment = await assessmentRepository.findOne({ where: { id: +req.body.assessment }, relations: ['asset'] }); if (!assessment) { return res.status(404).json('Assessment does not exist'); } const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } const vulnerability = new Vulnerability(); vulnerability.impact = req.body.impact; vulnerability.likelihood = req.body.likelihood; vulnerability.risk = req.body.risk; vulnerability.status = req.body.status; vulnerability.description = req.body.description; vulnerability.remediation = req.body.remediation; vulnerability.jiraId = req.body.jiraId || ''; vulnerability.cvssScore = req.body.cvssScore; vulnerability.cvssUrl = req.body.cvssUrl; vulnerability.detailedInfo = req.body.detailedInfo; vulnerability.assessment = assessment; vulnerability.name = req.body.name; vulnerability.systemic = req.body.systemic; const errors = await validate(vulnerability); if (errors.length > 0) { console.error('Validation errors:', errors); return res.status(400).send('Vulnerability form validation failed'); } const savedVulnerability = await vulnerabilityRepository.save(vulnerability); // Save screenshots if (req.files && req.files.length > 0) { await saveScreenshots(req.files, savedVulnerability); } // Save problem locations if (req.body.problemLocations) { try { const problemLocations = JSON.parse(req.body.problemLocations); await saveProblemLocations(problemLocations, savedVulnerability); } catch (error) { console.error('Error saving problem locations:', error); } } // Save resources if (req.body.resources) { try { const resources = JSON.parse(req.body.resources); await saveResources(resources, savedVulnerability); } catch (error) { console.error('Error saving resources:', error); } } return res.status(200).json('Vulnerability saved successfully'); } catch (error) { console.error('Error creating vulnerability:', error); return res.status(500).json('An error occurred while creating the vulnerability'); } }; /** * @description Save screenshots to vulnerability * @param {File[]} files * @param {Vulnerability} vulnerability */ export const saveScreenshots = async (files: File[], vulnerability: Vulnerability) => { const fileRepository = AppDataSource.getRepository(File); for (const screenshot of files) { try { let file = new File(); file = screenshot; file.vulnerability = vulnerability; const fileErrors = await validate(file); if (fileErrors.length > 0) { console.error('File validation failed:', fileErrors); continue; } await fileRepository.save(file); } catch (error) { console.error('Error saving screenshot:', error); } } }; /** * @description Save problem locations to vulnerability * @param {ProblemLocation[]} problemLocations * @param {Vulnerability} vulnerability */ export const saveProblemLocations = async (problemLocations: ProblemLocation[], vulnerability: Vulnerability) => { const problemLocationRepository = AppDataSource.getRepository(ProblemLocation); for (const probLoc of problemLocations) { try { if (probLoc && probLoc.location && probLoc.target) { let problemLocation = new ProblemLocation(); // If updating an existing problem location if (probLoc.id) { problemLocation = await problemLocationRepository.findOne({ where: { id: probLoc.id } }); if (!problemLocation) { problemLocation = new ProblemLocation(); } } problemLocation.location = probLoc.location; problemLocation.target = probLoc.target; problemLocation.vulnerability = vulnerability; const plErrors = await validate(problemLocation); if (plErrors.length > 0) { console.error('Problem Location validation failed:', plErrors); continue; } await problemLocationRepository.save(problemLocation); } } catch (error) { console.error('Error saving problem location:', error); } } }; /** * @description Save resources to vulnerability * @param {Resource[]} resources * @param {Vulnerability} vulnerability */ export const saveResources = async (resources: Resource[], vulnerability: Vulnerability) => { const resourceRepository = AppDataSource.getRepository(Resource); for (const resource of resources) { try { if (resource.description && resource.url) { let newResource = new Resource(); // If updating an existing resource if (resource.id) { newResource = await resourceRepository.findOne({ where: { id: resource.id } }); if (!newResource) { newResource = new Resource(); } } newResource.description = resource.description; newResource.url = resource.url; newResource.vulnerability = vulnerability; const nrErrors = await validate(newResource); if (nrErrors.length > 0) { console.error('Resource validation failed:', nrErrors); continue; } await resourceRepository.save(newResource); } } catch (error) { console.error('Error saving resource:', error); } } }; /** * @description Generate JIRA Ticket from Vuln * @param {UserRequest} req * @param {Response} res * @returns JIRA return ID? */ export const exportToJira = async (req: UserRequest, res: Response) => { try { if (!req.params.vulnId) { return res.status(400).json('Invalid Vulnerability ID'); } if (isNaN(+req.params.vulnId)) { return res.status(400).json('Invalid Vulnerability ID'); } const vulnerabilityRepository = AppDataSource.getRepository(Vulnerability); const assessmentRepository = AppDataSource.getRepository(Assessment); const jiraRepository = AppDataSource.getRepository(Jira); const vuln = await vulnerabilityRepository.findOne({ where: { id: +req.params.vulnId }, relations: ['screenshots', 'resources', 'problemLocations', 'assessment'] }); if (!vuln) { return res.status(404).json('Vulnerability not found'); } const assessment = await assessmentRepository.findOne({ where: { id: vuln.assessment.id }, relations: ['asset'] }); const hasAccess = await hasAssetWriteAccess(req, assessment.asset.id); if (!hasAccess) { return res.status(403).json('Authorization is required'); } const jira = await jiraRepository.findOne({ where: { asset: { id: assessment.asset.id } } }); if (!assessment.jiraId) { return res .status(400) .json( 'Unable to create JIRA ticket. Assessment requires an associated Jira ticket.' ); } if (!jira) { return res .status(400) .json( 'Unable to create JIRA ticket. Please provide JIRA credentials to the parent Asset.' ); } const jiraInit: JiraInit = { apiKey: jira.apiKey, host: jira.host, username: jira.username, }; try { const result = await exportToJiraIssue(vuln, jiraInit); vuln.jiraId = `https://${jiraInit.host}/browse/${result.key}`; await vulnerabilityRepository.save(vuln); return res.status(200).json(`${result.message}`); } catch (err) { return res.status(404).json(err); } } catch (error) { console.error('Error exporting to Jira:', error); return res.status(500).json('An error occurred while exporting to Jira'); } }; ================================================ FILE: src/services/email.service.spec.ts ================================================ import nodemailer = require('nodemailer'); import * as emailService from './email.service'; describe('email service', () => { test('send email failure missing credentials', async () => { const mailOptions = { from: 'pentester3@gmail.com', subject: 'Bulwark - Please confirm your email address', text: `Please confirm your email address \n A Bulwark account was created with the email: pentester@gmail.com. As an extra security measure, please verify this is the correct email address linked to Bulwark by clicking the link below. \n dev/api/user/verify/123`, to: 'pentester2@gmail.com' }; await emailService.sendEmail(mailOptions, (err, data) => { expect(err).toBe('Error sending email'); }); }); test('send verification email success', async () => { const uuid = 'abc'; const userEmail = 'pentester@gmail.com'; const spy = jest.spyOn(emailService, 'sendEmail'); await emailService.sendVerificationEmail(uuid, userEmail); expect(spy).toHaveBeenCalled(); }); test('send forgot password email success', async () => { const uuid = 'abc'; const userEmail = 'pentester@gmail.com'; const spy = jest.spyOn(emailService, 'sendEmail'); await emailService.sendForgotPasswordEmail(uuid, userEmail); expect(spy).toHaveBeenCalled(); }); test('send invitation email', async () => { const uuid = 'abc'; const userEmail = 'pentester@gmail.com'; const spy = jest.spyOn(emailService, 'sendEmail'); await emailService.sendInvitationEmail(uuid, userEmail); expect(spy).toHaveBeenCalled(); }); }); ================================================ FILE: src/services/email.service.ts ================================================ import * as nodemailer from 'nodemailer'; import { AppDataSource } from '../data-source'; import { Config } from '../entity/Config'; import { decrypt } from '../utilities/crypto.utility'; import { User } from '../entity/User'; interface MailOptions { from: string; to: string; subject: string; text: string; html?: string; } interface EmailCallback { (error: string | null, info: string | null): void; } /** * @description Send email * @param {MailOptions} mailOptions * @param {EmailCallback} callback */ export const sendEmail = async (mailOptions: MailOptions, callback: EmailCallback) => { try { const configRepository = AppDataSource.getRepository(Config); const config = await configRepository.findOne({ where: { id: 1 } }); if (!config || !config.fromEmail || !config.fromEmailPassword) { console.error('Missing email configuration'); return callback('Missing email configuration', null); } // Decrypt the email password let decryptedEmailPassword: string; try { decryptedEmailPassword = decrypt(config.fromEmailPassword); } catch (error) { console.error('Error decrypting email password:', error); return callback('Error with email configuration', null); } // Create a nodemailer transporter const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: config.fromEmail, pass: decryptedEmailPassword, }, }); // Send the email transporter.sendMail(mailOptions, (error, info) => { if (error) { console.error('Email sending error:', error); return callback('Error sending email', null); } else { return callback(null, 'Email sent successfully'); } }); } catch (error) { console.error('Email service error:', error); return callback('Email service error', null); } }; /** * @description Prepare account verification email * @param {string} uuid * @param {string} userEmail */ export const sendVerificationEmail = (uuid: string, userEmail: string) => { const serverAddress = process.env.SERVER_ADDRESS || 'http://localhost'; const serverPort = process.env.PORT || '5000'; const mailOptions = { from: process.env.FROM_EMAIL || 'noreply@bulwark.com', subject: 'Bulwark - Please confirm your email address', text: `Please confirm your email address A Bulwark account was created with the email: ${userEmail}. As an extra security measure, please verify this is the correct email address linked to Bulwark by clicking the link below. ${serverAddress}:${serverPort}/api/user/verify/${uuid}`, to: userEmail, }; sendEmail(mailOptions, (err, info) => { if (err) { console.error('Error sending verification email:', err); } else { console.log('Verification email sent successfully'); } }); }; /** * @description Prepare password reset email * @param {string} uuid * @param {string} userEmail */ export const sendForgotPasswordEmail = (uuid: string, userEmail: string) => { const serverAddress = process.env.SERVER_ADDRESS || 'http://localhost'; const serverPort = process.env.PORT || '5000'; const mailOptions = { from: process.env.FROM_EMAIL || 'noreply@bulwark.com', subject: 'Bulwark - Forgot Password Request', text: `Forgot password request A password request has been initiated for the email: ${userEmail}. Please click the link below to initiate the process. ${serverAddress}:${serverPort}/#/password-reset/${uuid}`, to: userEmail, }; sendEmail(mailOptions, (err, info) => { if (err) { console.error('Error sending forgot password email:', err); } else { console.log('Forgot password email sent successfully'); } }); }; /** * @description Prepare invitation email * @param {string} uuid * @param {string} userEmail */ export const sendInvitationEmail = (uuid: string, userEmail: string) => { const serverAddress = process.env.SERVER_ADDRESS || 'http://localhost'; const serverPort = process.env.PORT || '5000'; const mailOptions = { from: process.env.FROM_EMAIL || 'noreply@bulwark.com', subject: 'Bulwark - Welcome!', text: `You have been invited to Bulwark! Please click the link below to initiate the process. ${serverAddress}:${serverPort}/#/register/${uuid}`, to: userEmail, }; sendEmail(mailOptions, (err, info) => { if (err) { console.error('Error sending invitation email:', err); } else { console.log('Invitation email sent successfully'); } }); }; /** * @description Prepare email update request * @param {User} user * @param {EmailCallback} callback */ export const sendUpdateUserEmail = (user: User, callback: EmailCallback) => { const serverAddress = process.env.SERVER_ADDRESS || 'http://localhost'; const serverPort = process.env.PORT || '5000'; const mailOptions = { from: process.env.FROM_EMAIL || 'noreply@bulwark.com', subject: 'Bulwark - Email update request', text: `An email update has been requested for Bulwark. Please click the link to confirm this update. ${serverAddress}:${serverPort}/#/email/validate/${user.uuid}`, to: user.newEmail, }; sendEmail(mailOptions, (err, info) => { if (err) { callback(err, null); } else { callback(null, info); } }); }; ================================================ FILE: src/temp/empty.ts ================================================ // The purpose of this file is to ensure the temp directory is created during ts compilation ================================================ FILE: src/utilities/column-mapper.utility.ts ================================================ import { Column, ColumnType, ColumnOptions } from 'typeorm'; const mysqlSqliteTypeMapping: { [key: string]: ColumnType } = { mediumblob: 'blob' }; const resolveDbType = (mySqlType: ColumnType): ColumnType => { const isTestEnv = process.env.NODE_ENV === 'test'; if (isTestEnv && mySqlType.toString() in mysqlSqliteTypeMapping) { return mysqlSqliteTypeMapping[mySqlType.toString()]; } return mySqlType; }; /** * @description Wrapper function for resolving DB Type * @param {ColumnOptions} columnOptions * @returns Custom Column */ export const DbAwareColumn = (columnOptions: ColumnOptions) => { if (columnOptions.type) { columnOptions.type = resolveDbType(columnOptions.type); } return Column(columnOptions); }; /** * @description Addes nullabe column option if test DB is active * @returns Custom Column */ export const dynamicNullable = () => { const columnOptions: ColumnOptions = { nullable: process.env.NODE_ENV === 'test' ? true : false }; return Column(columnOptions); }; ================================================ FILE: src/utilities/crypto.utility.spec.ts ================================================ import { encrypt, decrypt } from './crypto.utility'; describe('crypto utility', () => { test('decrypt', () => { const str = encrypt('password'); expect(decrypt(str)).toBe('password'); }); }); ================================================ FILE: src/utilities/crypto.utility.ts ================================================ import * as crypto from 'crypto'; // Check for required environment variables if (!process.env.CRYPTO_SECRET || !process.env.CRYPTO_SALT) { console.error('CRYPTO_SECRET and CRYPTO_SALT environment variables must be set!'); } const algorithm = 'aes-256-cbc'; const key = crypto.scryptSync( process.env.CRYPTO_SECRET || 'default-secret-key-for-dev-only', process.env.CRYPTO_SALT || 'default-salt-for-dev-only', 32 ); /** * @description Encrypt text using AES-256-CBC * @param {string} text - The text to encrypt * @returns {string} - JSON stringified object with IV and encrypted data */ export const encrypt = (text: string): string => { try { // Generate a random 16-byte initialization vector const iv = crypto.randomBytes(16); // Create cipher const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv); // Encrypt the text let encrypted = cipher.update(text); encrypted = Buffer.concat([encrypted, cipher.final()]); // Return IV and encrypted data return JSON.stringify({ iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }); } catch (error) { console.error('Encryption error:', error); throw new Error('Failed to encrypt data'); } }; /** * @description Decrypt text that was encrypted with encrypt() * @param {string} text - JSON stringified object with IV and encrypted data * @returns {string} - The decrypted text */ export const decrypt = (text: string): string => { try { // Parse the JSON string const parsedText = JSON.parse(text); // Convert hex strings to buffers const iv = Buffer.from(parsedText.iv, 'hex'); const encryptedText = Buffer.from(parsedText.encryptedData, 'hex'); // Create decipher const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv); // Decrypt the text let decrypted = decipher.update(encryptedText); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); } catch (error) { console.error('Decryption error:', error); throw new Error('Failed to decrypt data'); } }; ================================================ FILE: src/utilities/file.utility.ts ================================================ import multer = require('multer'); import * as path from 'path'; import * as fs from 'fs'; // Create temp directory if it doesn't exist const tempDir = path.join(__dirname, '../temp'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const maxFileSize = 5000000; // 5 MB const fileFilter = (req, file, cb) => { // Ext validation if (!(file.mimetype === 'image/png' || file.mimetype === 'image/jpeg')) { req.fileExtError = 'Only JPEG and PNG file types allowed'; cb(null, false); } else { cb(null, true); } }; export const upload = multer({ fileFilter, limits: { fileSize: maxFileSize } }).single('file'); export const uploadArray = multer({ fileFilter, limits: { fileSize: maxFileSize } }).array('screenshots'); ================================================ FILE: src/utilities/jira.utility.spec.ts ================================================ import * as jiraUtility from './jira.utility'; import { createConnection, getConnection } from 'typeorm'; import { Asset } from '../entity/Asset'; import { Organization } from '../entity/Organization'; import { Vulnerability } from '../entity/Vulnerability'; import { Assessment } from '../entity/Assessment'; import { User } from '../entity/User'; import { ProblemLocation } from '../entity/ProblemLocation'; import { Resource } from '../entity/Resource'; import { File } from '../entity/File'; import { Jira } from '../entity/Jira'; import { Team } from '../entity/Team'; import { ApiKey } from '../entity/ApiKey'; import { Config } from '../entity/Config'; import { ReportAudit } from '../entity/ReportAudit'; // TODO: Figure out how to mock jira-client API calls so that we do not hit the atlassian API's describe('jira utility db', () => { beforeEach(async () => { await createConnection({ type: 'sqlite', database: ':memory:', dropSchema: true, entities: [ Config, User, Team, Organization, Asset, Assessment, Vulnerability, ProblemLocation, ReportAudit, Resource, Jira, File, ApiKey, ], synchronize: true, logging: false, name: 'default', }); }); afterEach(() => { const conn = getConnection('default'); return conn.close(); }); test('map risk to severity informational', () => { let risk = 'Informational'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe('Lowest'); risk = 'Low'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe('Low'); risk = 'Medium'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe('Medium'); risk = 'High'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe('High'); risk = 'Critical'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe('Highest'); risk = 'Random'; expect(jiraUtility.mapRiskToSeverity(risk)).toBe(''); }); test('map vuln to jira issue', async () => { const resources: Resource[] = []; const probLoc: ProblemLocation[] = []; const assessment: Assessment = null; const vuln: Vulnerability = { id: 1, jiraId: '', impact: 'low', risk: 'low', likelihood: 'low', systemic: 'Yes', cvssScore: 1.0, status: 'Open', description: 'Test description', detailedInfo: 'Test detailed description', remediation: 'Test remediation', name: 'SQL Injection', resources, problemLocations: probLoc, assessment, cvssUrl: 'http://test.com', screenshots: null, }; const firstProbLoc: ProblemLocation = { id: 1, target: '', vulnerability: vuln, location: '', }; const resource: Resource = { id: 1, description: 'test', vulnerability: vuln, url: 'http://test.com', }; vuln.problemLocations.push(firstProbLoc); vuln.resources.push(resource); expect( await jiraUtility.mapVulnToJiraIssue(vuln, 'test-123') ).toBeDefined(); }); test('get issue key', () => { const jiraUrl = 'https://bulwark-test.atlassian.net/browse/tst-1'; expect(jiraUtility.getIssueKey(jiraUrl)).toBe('tst-1'); }); }); ================================================ FILE: src/utilities/jira.utility.ts ================================================ import { JiraIssue } from 'src/interfaces/jira/jira-issue.interface'; import { Vulnerability } from '../entity/Vulnerability'; import { JiraInit } from 'src/interfaces/jira/jira-init.interface'; import { decrypt } from './crypto.utility'; import { JiraResult } from 'src/interfaces/jira/jira-result.interface'; import * as fs from 'fs'; import * as path from 'path'; import * as mime from 'mime-types'; import { IssueLink } from 'src/interfaces/jira/jira-issue-link.interface'; // Use fetch-node version 2 with CommonJS compatibility const fetch = require('node-fetch'); const j2m = require('jira2md'); const JiraApi = require('jira-client'); let jira = null; /** * @description Entry function to create or update a JIRA ticket associated to a vulnerability * @param {JiraInit} jiraInit * @param {Vulnerability} vulnerability * @returns success: return object errror: error message */ /* istanbul ignore next */ export const exportToJiraIssue = (vuln: Vulnerability, jiraInit: JiraInit): Promise => { return new Promise(async (resolve, reject) => { try { initializeJira(jiraInit); const assessment = vuln.assessment; const parentKey = getIssueKey(assessment.jiraId); let assessmentIssue; try { assessmentIssue = await jira.getIssue(parentKey); } catch (err) { console.error('Error fetching Jira issue:', err); reject( `An error has occurred. The JIRA issue ${getIssueKey( vuln.jiraId )} does not exist. Please update the JIRA URL and try again` ); return; } const jiraIssue = await mapVulnToJiraIssue(vuln, assessmentIssue.fields.project.id); if (!vuln.jiraId) { try { const result = await addNewJiraIssue(jiraIssue, parentKey, vuln); resolve(result); } catch (err) { console.error('Error adding new Jira issue:', err); reject(err); return; } } else { try { const result = await updateExistingJiraIssue( jiraIssue, parentKey, vuln, assessmentIssue.fields.project.id, jiraInit ); resolve(result); } catch (err) { console.error('Error updating existing Jira issue:', err); reject(err); return; } } } catch (error) { console.error('Error in exportToJiraIssue:', error); reject('An error occurred while exporting to Jira'); } }); }; /** * @description Add new Jira issue * @param {any} jiraIssue * @param {string} parentUrl * @param {Vulnerability} vuln * @returns Jira result */ /* istanbul ignore next */ const addNewJiraIssue = (jiraIssue: any, parentKey: string, vuln: Vulnerability): Promise => { return new Promise(async (resolve, reject) => { try { let saved: any; try { saved = await jira.addNewIssue(jiraIssue); if (parentKey) { await issueLink(parentKey, saved.key, (err, res) => { if (err) { console.error('Error linking issues:', err); } }); } } catch (err) { console.error('Error creating Jira issue:', err); reject('The Jira export has failed.'); return; } const returnObj: JiraResult = { id: saved.id, key: saved.key, self: saved.self, message: `The vulnerability for "${vuln.name}" has been exported to Jira. Key: ${saved.key}` }; if (vuln.screenshots && vuln.screenshots.length > 0) { await attachImages(vuln, returnObj.id); } resolve(returnObj); } catch (error) { console.error('Error in addNewJiraIssue:', error); reject('An error occurred while adding a new Jira issue'); } }); }; /** * @description Update existing Jira issue * @param {any} jiraIssue * @param {string} parentUrl * @param {Vulnerability} vuln * @returns Jira result */ /* istanbul ignore next */ const updateExistingJiraIssue = ( jiraIssue: any, parentKey: string, vuln: Vulnerability, projectId: string, jiraInit: JiraInit ): Promise => { return new Promise(async (resolve, reject) => { try { let issueKey: string; let existingIssue: any; try { issueKey = getIssueKey(vuln.jiraId); existingIssue = await jira.getIssue(issueKey); const updatedJiraIssue = await mapVulnToJiraIssue(vuln, projectId); await jira.updateIssue(existingIssue.id, updatedJiraIssue); if (parentKey) { await issueLink(parentKey, existingIssue.key, (err, res) => { if (err) { console.error('Error linking issues:', err); } }); } } catch (err) { console.error('Error updating Jira issue:', err); reject( `An error has occurred. The JIRA issue ${issueKey} does not exist. Please update the Jira field with a valid URL and try again.` ); return; } // Process attachments if (vuln.screenshots && vuln.screenshots.length > 0) { // Delete existing attachments if (existingIssue.fields.attachment && existingIssue.fields.attachment.length > 0) { for (const existScreenshot of existingIssue.fields.attachment) { await deleteIssueAttachment(existScreenshot.id, jiraInit); } } // Add new attachments await attachImages(vuln, existingIssue.id); } const returnObj: JiraResult = { id: existingIssue.id, key: existingIssue.key, self: existingIssue.self, message: `The vulnerability for "${vuln.name}" has been updated in JIRA. Key: ${existingIssue.key}` }; resolve(returnObj); } catch (error) { console.error('Error in updateExistingJiraIssue:', error); reject('An error occurred while updating the Jira issue'); } }); }; /** * @description Links Jira ticket to parent ticket * @param callback * @param {string} issueKey * @param {string} parentUrl * @returns success: links jira issue to parent ticket */ /* istanbul ignore next */ const issueLink = async (parentUrl: string, issueKey: string, callback) => { try { const parentKey = getIssueKey(parentUrl); const link: IssueLink = { outwardIssue: { key: parentKey }, inwardIssue: { key: issueKey }, type: { name: 'Blocks' } }; try { await jira.issueLink(link); return; } catch (err) { console.error('Error linking Jira issues:', err); callback(`The JIRA Project "${parentKey}" does not exist.`); } } catch (error) { console.error('Error in issueLink:', error); callback('An error occurred while linking issues'); } }; /** * @description Deletes Jira ticket attachment * @param {string} id * @param {JiraInit} jiraInit * @returns success: return object errror: error message */ /* istanbul ignore next */ const deleteIssueAttachment = async (id: string, jiraInit: JiraInit) => { try { const auth = `${jiraInit.username}:${decrypt(jiraInit.apiKey)}`; const response = await fetch(`https://${jiraInit.host}/rest/api/3/attachment/${id}`, { method: 'DELETE', headers: { Authorization: `Basic ${Buffer.from(auth).toString('base64')}` } }); console.info(`Attachment deletion status: ${response.status} ${response.statusText}`); } catch (err) { console.error('Error deleting Jira attachment:', err); } }; /** * @description Returns Jira issue key * @param {string} url * @returns string of key */ export const getIssueKey = (url: string): string => { try { if (!url) { return ''; } const ary: string[] = url.split('/'); return ary[ary.length - 1]; } catch (error) { console.error('Error getting issue key:', error); return ''; } }; /** * @description Initializes Jira * @param {JiraInit} jiraInit * @returns nothing */ /* istanbul ignore next */ const initializeJira = (jiraInit: JiraInit) => { try { jira = new JiraApi({ protocol: 'https', host: jiraInit.host, username: jiraInit.username, password: decrypt(jiraInit.apiKey), apiVersion: '3', strictSSL: true }); } catch (error) { console.error('Error initializing Jira:', error); throw new Error('Failed to initialize Jira client'); } }; /** * @description Attaches one-to-many images to Jira ticket * @param {string} issueId * @param {Vulnerability} vulnerability * @returns nothing */ /* istanbul ignore next */ const attachImages = async (vuln: Vulnerability, issueId: string) => { try { if (!vuln.screenshots || vuln.screenshots.length === 0) { return; } // Create temp directory if it doesn't exist const tempDir = path.join(__dirname, '../temp'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } for (const screenshot of vuln.screenshots) { try { // Get file extension from mimetype const extension = mime.extension(screenshot.mimetype) || 'jpg'; const filename = screenshot.originalname || `screenshot_${Date.now()}.${extension}`; const filepath = path.join(tempDir, filename); // Write buffer to temporary file await fs.writeFileSync(filepath, screenshot.buffer); const stream = fs.createReadStream(filepath); // Add attachment to Jira issue await jira.addAttachmentOnIssue(issueId, stream); // Clean up temporary file await fs.unlinkSync(filepath); } catch (err) { console.error('Error attaching image to Jira issue:', err); } } } catch (error) { console.error('Error in attachImages:', error); } }; /** * @description Maps Vulnerability information to Jira ticket * @param {string} projectId * @param {Vulnerability} vulnerability * @returns JiraIssue object */ export const mapVulnToJiraIssue = async (vuln: Vulnerability, projectId: string) => { try { const probLocRows = await dynamicProbLocTableRows(vuln); const resourceRows = await dynamicResourceTableRows(vuln); const jiraIssue: JiraIssue = { update: {}, fields: { project: { id: projectId.toString() }, priority: { name: mapRiskToSeverity(vuln.risk) }, summary: vuln.name, description: { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: `Impact: ${vuln.impact}`, marks: [ { type: 'strong' } ] } ] }, { type: 'paragraph', content: [ { type: 'text', text: `Likelihood: ${vuln.likelihood}`, marks: [ { type: 'strong' } ] } ] }, { type: 'paragraph', content: [ { type: 'text', text: `Overall Risk: ${vuln.risk}`, marks: [ { type: 'strong' } ] } ] }, { type: 'paragraph', content: [ { type: 'text', text: `Systemic: ${vuln.systemic}`, marks: [ { type: 'strong' } ] } ] }, { type: 'paragraph', content: [ { type: 'text', text: `CVSS Score: ${vuln.cvssScore}`, marks: [ { type: 'link', attrs: { href: vuln.cvssUrl } } ] } ] }, { type: 'table', attrs: { isNumberColumnEnabled: false, layout: 'default' }, content: probLocRows }, { type: 'paragraph', content: [ { text: j2m.to_jira(vuln.description || ''), type: 'text' } ] }, { type: 'paragraph', content: [ { text: j2m.to_jira(vuln.detailedInfo || ''), type: 'text' } ] }, { type: 'paragraph', content: [ { type: 'text', text: 'Remediation', marks: [ { type: 'strong' } ] } ] }, { type: 'paragraph', content: [ { text: j2m.to_jira(vuln.remediation || ''), type: 'text' } ] }, { type: 'paragraph', content: [ { type: 'text', text: 'Resources', marks: [ { type: 'strong' } ] } ] }, { type: 'table', attrs: { isNumberColumnEnabled: false, layout: 'default' }, content: resourceRows } ] }, issuetype: { name: 'Bug' } } }; return jiraIssue; } catch (error) { console.error('Error mapping vulnerability to Jira issue:', error); throw new Error('Failed to map vulnerability to Jira issue'); } }; /** * @description Dynamically creates Jira table for problem locations * @param {Vulnerability} vulnerability * @returns table rows */ const dynamicProbLocTableRows = async (vuln: Vulnerability) => { try { const rows = []; rows.push({ type: 'tableRow', content: [ { type: 'tableHeader', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Problem Location' } ] } ] }, { type: 'tableHeader', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Target' } ] } ] } ] }); if (vuln.problemLocations && vuln.problemLocations.length) { for (const probLoc of vuln.problemLocations) { const row = { type: 'tableRow', content: [ { type: 'tableCell', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: probLoc.location || '' } ] } ] }, { type: 'tableCell', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: probLoc.target || '' } ] } ] } ] }; rows.push(row); } } return rows; } catch (error) { console.error('Error creating problem location table rows:', error); return []; } }; /** * @description Dynamically creates Jira table for resources * @param {Vulnerability} vulnerability * @returns table rows */ const dynamicResourceTableRows = async (vuln: Vulnerability) => { try { const rows = []; rows.push({ type: 'tableRow', content: [ { type: 'tableHeader', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Description' } ] } ] }, { type: 'tableHeader', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Resource URL' } ] } ] } ] }); if (vuln.resources && vuln.resources.length) { for (const resource of vuln.resources) { const row = { type: 'tableRow', content: [ { type: 'tableCell', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: resource.description || '' } ] } ] }, { type: 'tableCell', attrs: {}, content: [ { type: 'paragraph', content: [ { type: 'text', text: resource.url || '', marks: [ { type: 'link', attrs: { href: resource.url } } ] } ] } ] } ] }; rows.push(row); } } return rows; } catch (error) { console.error('Error creating resource table rows:', error); return []; } }; /** * @description Maps overall risk severity to Jira priority * @param {Vulnerability} vulnerability * @returns jira priority string */ export const mapRiskToSeverity = (risk: string) => { switch (risk) { case 'Informational': return 'Lowest'; case 'Low': return 'Low'; case 'Medium': return 'Medium'; case 'High': return 'High'; case 'Critical': return 'Highest'; default: return ''; } }; ================================================ FILE: src/utilities/password.utility.spec.ts ================================================ import { generateHash, updatePassword, compare } from './password.utility'; describe('password utility', () => { test('generateHash to work', async () => { const password = 'qwerty'; const hashedPassword = await generateHash(password); expect(hashedPassword).toBeDefined(); }); test('compare hash success', async () => { const oldPassword = await generateHash('qwerty'); const currentPassword = 'qwerty'; await expect(compare(currentPassword, oldPassword)).resolves.toBeTruthy(); }); test('compare hash failure', async () => { const oldPassword = await generateHash('qwerty'); const currentPassword = 'qwerty2'; await expect(compare(currentPassword, oldPassword)).resolves.toBeFalsy(); }); test('compare hash bcrypt failure', async () => { await expect(compare(null, 'a')).rejects.toBe('Bcrypt comparison failure'); }); test('password updated successfully', async () => { const currentPassword = 'qwerty'; const oldPassword = await generateHash(currentPassword); const newPassword = 'newQwerty'; const result = await updatePassword( oldPassword, currentPassword, newPassword ); expect(result).toEqual(expect.anything()); }); test('password updated failure', async () => { const currentPassword = 'qwerty2'; const oldPassword = await generateHash('1234'); const newPassword = 'newQwerty'; await expect( updatePassword(oldPassword, currentPassword, newPassword) ).rejects.toBe('The current password is incorrect'); }); }); ================================================ FILE: src/utilities/password.utility.ts ================================================ import * as bcrypt from 'bcrypt'; // tslint:disable-next-line: no-var-requires const passwordValidator = require('password-validator'); // Create a password schema with requirements export const passwordSchema = new passwordValidator(); passwordSchema .is() .min(12) // Minimum length 12 .has() .uppercase() // Must have uppercase letters .has() .lowercase() // Must have lowercase letters .has() .digits() // Must have digits .has() .symbols(); // Must have symbols // Number of salt rounds for bcrypt const saltRounds = 10; /** * @description Generate hash from password * @param {string} password - The plaintext password * @returns {Promise} - The hashed password */ export const generateHash = (password: string): Promise => { return new Promise((resolve, reject) => { bcrypt.genSalt(saltRounds, async (err, salt) => { if (err) { console.error('bcrypt salt generation error:', err); reject('Bcrypt hash failed: ' + err); } else { bcrypt.hash(password, salt, async (hashErr, hash: string) => { if (hashErr) { console.error('bcrypt hash error:', hashErr); reject('Bcrypt hash failed: ' + hashErr); } else { resolve(hash); } }); } }); }); }; /** * @description Update user password * @param {string} hashedCurrentPassword - The current hashed password * @param {string} currentPassword - The plaintext current password * @param {string} newPassword - The plaintext new password * @returns {Promise} - The newly hashed password */ export const updatePassword = ( hashedCurrentPassword: string, currentPassword: string, newPassword: string ): Promise => { return new Promise(async (resolve, reject) => { try { // Verify the current password const valid = await compare(currentPassword, hashedCurrentPassword); if (!valid) { reject('The current password is incorrect'); return; } // Generate a hash for the new password const newPasswordHash = await generateHash(newPassword); resolve(newPasswordHash); } catch (error) { console.error('Password update error:', error); reject('An error occurred while updating the password'); } }); }; /** * @description Compare password hash * @param {string|string[]} currentPassword - The plaintext password to check * @param {string} hashedCurrentPassword - The stored hashed password * @returns {Promise} - Whether the password is valid */ export const compare = ( currentPassword: string | string[], hashedCurrentPassword: string ): Promise => { return new Promise((resolve, reject) => { if (!currentPassword || !hashedCurrentPassword) { reject('Password or hash is missing'); return; } bcrypt.compare(currentPassword, hashedCurrentPassword, (err, valid) => { if (err) { console.error('bcrypt comparison error:', err); reject('Bcrypt comparison failure'); return; } resolve(valid); }); }); }; ================================================ FILE: src/utilities/puppeteer.utility.ts ================================================ import { UserRequest } from '../interfaces/user-request.interface'; import { Response } from 'express'; import { insertReportAuditRecord } from '../routes/report-audit.controller'; import puppeteer from 'puppeteer'; import * as path from 'path'; import * as fs from 'fs'; import { hasAssetReadAccess } from './role.utility'; /** * @description API backend for report generation with Puppeteer * @param {UserRequest} req orgId, assetId, assessmentId * @param {Response} res contains all data associated and generates a * new html page with PDF Report * @returns a new page generated by Puppeteer with a Report in PDF format */ export const generateReport = async (req: UserRequest, res: Response) => { try { if (!req.body.orgId || !req.body.assetId || !req.body.assessmentId) { return res.status(400).send('Invalid report parameters'); } const hasReadAccess = await hasAssetReadAccess(req, +req.body.assetId); if (!hasReadAccess) { return res .status(404) .json('Failed to generate report. Please contact an administrator.'); } const url = process.env.NODE_ENV === 'production' ? `${process.env.SERVER_ADDRESS}:${process.env.PORT}/#/organization/${req.body.orgId}/asset/${req.body.assetId}/assessment/${req.body.assessmentId}/report/puppeteer` : `${process.env.DEV_URL}/#/organization/${req.body.orgId}/asset/${req.body.assetId}/assessment/${req.body.assessmentId}/report/puppeteer`; // Create temp directory if it doesn't exist const tempDir = path.join(__dirname, '../temp'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const filePath = path.join(tempDir, 'temp_report.pdf'); const jwtToken = req.headers.authorization; const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: true // Use standard headless mode for compatibility }); const page = await browser.newPage(); // Set JWT token in localStorage await page.evaluateOnNewDocument((token) => { localStorage.clear(); localStorage.setItem('AUTH_TOKEN', token); }, jwtToken); await page.goto(url, { waitUntil: 'networkidle0' }); // Generate PDF await page.pdf({ path: filePath, format: 'a4', printBackground: true, margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' } }); await browser.close(); // Send file to client const file = fs.createReadStream(filePath); const stat = fs.statSync(filePath); res.setHeader('Content-Length', stat.size); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename=report.pdf'); file.pipe(res); // Record the report generation await insertReportAuditRecord(+req.user, req.body.assessmentId); // Delete the temporary file after it's sent file.on('close', () => { fs.unlink(filePath, (err) => { if (err) { console.error('Error removing temporary PDF file:', err); } else { console.info('Temporary PDF file removed'); } }); }); } catch (error) { console.error('Error generating report:', error); return res.status(500).json('An error occurred while generating the report'); } }; ================================================ FILE: src/utilities/role.utility.ts ================================================ import { ROLE } from '../enums/roles-enum'; import { UserRequest } from '../interfaces/user-request.interface'; /** * @description Check if user has access to an organization * @param {UserRequest} req * @param {number} rqstdOrgId * @returns boolean */ export const hasOrgAccess = (req: UserRequest, rqstdOrgId: number): boolean => { if (!req.userOrgs || !req.userOrgs.length) { return false; } return req.userOrgs.includes(rqstdOrgId); }; /** * @description Check if user has read access to an asset * @param {UserRequest} req * @param {number} assetId * @returns Promise */ export const hasAssetReadAccess = async (req: UserRequest, assetId: number): Promise => { // Ensure we have userAssets array if (!req.userAssets) { return false; } return req.userAssets.includes(assetId); }; /** * @description Check if user has write access to an asset * @param {UserRequest} req * @param {number} assetId * @returns Promise */ export const hasAssetWriteAccess = async (req: UserRequest, assetId: number): Promise => { // Admins have write access to all assets if (req.isAdmin) { return true; } // If user is not an admin, check if they belong to a tester team with access to this asset if (!req.userTeams || !req.userTeams.length) { return false; } const testerTeams = req.userTeams.filter((team) => team.role === ROLE.TESTER); if (!testerTeams || !testerTeams.length) { return false; } // Check if any of the user's tester teams have access to this asset for (const testerTeam of testerTeams) { if (!testerTeam.assets || !testerTeam.assets.length) { continue; } for (const asset of testerTeam.assets) { if (asset.id === assetId) { return true; } } } return false; }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": ["es6", "dom"], "target": "es2017", "module": "commonjs", "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "outDir": "dist", "removeComments": true, "baseUrl": "./", "sourceMap": true, "types": ["@types/jest"], }, "exclude": ["./frontend", "./migration"], "include": ["src/**/*"] } ================================================ FILE: tslint.json ================================================ { "extends": ["tslint:latest", "tslint-config-prettier"], "rules": { "max-line-length": { "options": [120] }, "quotemark": [true, "single", "avoid-escape", "avoid-template"], "new-parens": true, "no-arg": true, "no-bitwise": true, "no-conditional-assignment": true, "no-consecutive-blank-lines": false, "no-console": { "severity": "warning", "options": ["debug", "info", "log", "time", "timeEnd", "trace"] }, "arrow-parens": false, "trailing-comma": false, "ordered-imports": false, "no-var-requires": false }, "jsRules": { "max-line-length": { "options": [120] } } }