├── .dockerignore ├── .github └── workflows │ ├── create-release.yml │ ├── github-pages.yml │ └── run-jasmine-tests.yml ├── .gitignore ├── .markdownlint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── USAGE.md ├── layers ├── README.md ├── samples │ ├── ATTACKcon 2018 │ │ ├── Black_Pins.json │ │ ├── Blue_Pins.json │ │ ├── Gold_Pins.json │ │ ├── Red_Pins.json │ │ └── Submitter_Responses.json │ ├── Bear_APT.json │ └── heatmap_layer.json └── spec │ ├── v1.0 │ └── layerformat.md │ ├── v1.1 │ └── layerformat.md │ ├── v1.2 │ └── layerformat.md │ ├── v1.3 │ └── layerformat.md │ ├── v2.0 │ └── layerformat.md │ ├── v2.1 │ └── layerformat.md │ ├── v2.2 │ └── layerformat.md │ ├── v3.0 │ └── layerformat.md │ ├── v4.0 │ └── layerformat.md │ ├── v4.1 │ └── layerformat.md │ ├── v4.2 │ └── layerformat.md │ ├── v4.3 │ └── layerformat.md │ ├── v4.4 │ └── layerformat.md │ └── v4.5 │ └── layerformat.md └── nav-app ├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── redirects ├── enterprise │ └── index.html └── mobile │ └── index.html ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── changelog │ │ ├── changelog.component.html │ │ ├── changelog.component.spec.ts │ │ └── changelog.component.ts │ ├── classes │ │ ├── context-menu-item.ts │ │ ├── domain.ts │ │ ├── filter.ts │ │ ├── gradient.ts │ │ ├── index.ts │ │ ├── layout-options.ts │ │ ├── link.ts │ │ ├── metadata.ts │ │ ├── stix │ │ │ ├── asset.ts │ │ │ ├── campaign.ts │ │ │ ├── data-component.ts │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── matrix.ts │ │ │ ├── mitigation.ts │ │ │ ├── note.ts │ │ │ ├── software.ts │ │ │ ├── stix-object.ts │ │ │ ├── tactic.ts │ │ │ └── technique.ts │ │ ├── tab.ts │ │ ├── technique-vm.ts │ │ ├── version-changelog.ts │ │ ├── version.ts │ │ └── view-model.ts │ ├── datatable │ │ ├── data-table.component.html │ │ ├── data-table.component.scss │ │ └── data-table.component.ts │ ├── help │ │ ├── help.component.html │ │ ├── help.component.scss │ │ ├── help.component.spec.ts │ │ └── help.component.ts │ ├── layer-information │ │ ├── layer-information.component.html │ │ ├── layer-information.component.scss │ │ ├── layer-information.component.spec.ts │ │ └── layer-information.component.ts │ ├── layer-settings │ │ ├── layer-settings.component.html │ │ ├── layer-settings.component.scss │ │ ├── layer-settings.component.spec.ts │ │ └── layer-settings.component.ts │ ├── layer-upgrade │ │ ├── changelog-cell │ │ │ ├── changelog-cell.component.html │ │ │ ├── changelog-cell.component.scss │ │ │ ├── changelog-cell.component.spec.ts │ │ │ └── changelog-cell.component.ts │ │ ├── layer-upgrade.component.html │ │ ├── layer-upgrade.component.scss │ │ └── layer-upgrade.component.ts │ ├── list-input │ │ ├── list-input.component.html │ │ ├── list-input.component.scss │ │ ├── list-input.component.spec.ts │ │ └── list-input.component.ts │ ├── matrix │ │ ├── cell.ts │ │ ├── matrix-common.scss │ │ ├── matrix-common.spec.ts │ │ ├── matrix-common.ts │ │ ├── matrix-flat │ │ │ ├── matrix-flat.component.html │ │ │ ├── matrix-flat.component.scss │ │ │ ├── matrix-flat.component.spec.ts │ │ │ └── matrix-flat.component.ts │ │ ├── matrix-mini │ │ │ ├── matrix-mini.component.html │ │ │ ├── matrix-mini.component.scss │ │ │ ├── matrix-mini.component.spec.ts │ │ │ └── matrix-mini.component.ts │ │ ├── matrix-side │ │ │ ├── matrix-side.component.html │ │ │ ├── matrix-side.component.scss │ │ │ ├── matrix-side.component.spec.ts │ │ │ └── matrix-side.component.ts │ │ ├── tactic-cell │ │ │ ├── tactic-cell.component.html │ │ │ ├── tactic-cell.component.scss │ │ │ ├── tactic-cell.component.spec.ts │ │ │ └── tactic-cell.component.ts │ │ └── technique-cell │ │ │ ├── cell-popover.scss │ │ │ ├── cell-popover.ts │ │ │ ├── contextmenu │ │ │ ├── contextmenu.component.html │ │ │ ├── contextmenu.component.scss │ │ │ ├── contextmenu.component.spec.ts │ │ │ └── contextmenu.component.ts │ │ │ ├── technique-cell.component.html │ │ │ ├── technique-cell.component.scss │ │ │ ├── technique-cell.component.spec.ts │ │ │ ├── technique-cell.component.ts │ │ │ └── tooltip │ │ │ ├── tooltip.component.html │ │ │ ├── tooltip.component.scss │ │ │ ├── tooltip.component.spec.ts │ │ │ └── tooltip.component.ts │ ├── search-and-multiselect │ │ ├── search-and-multiselect.component.html │ │ ├── search-and-multiselect.component.scss │ │ └── search-and-multiselect.component.ts │ ├── services │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ ├── data.service.spec.ts │ │ ├── data.service.ts │ │ ├── icons.service.spec.ts │ │ ├── icons.service.ts │ │ ├── viewmodels.service.spec.ts │ │ └── viewmodels.service.ts │ ├── sidebar │ │ ├── sidebar.component.html │ │ ├── sidebar.component.scss │ │ ├── sidebar.component.spec.ts │ │ └── sidebar.component.ts │ ├── svg-export │ │ ├── renderable-objects │ │ │ ├── index.ts │ │ │ ├── renderable-matrix.ts │ │ │ ├── renderable-tactic.ts │ │ │ └── renderable-technique.ts │ │ ├── svg-export.component.html │ │ ├── svg-export.component.scss │ │ ├── svg-export.component.spec.ts │ │ └── svg-export.component.ts │ ├── tabs │ │ ├── tabs.component.html │ │ ├── tabs.component.scss │ │ ├── tabs.component.spec.ts │ │ └── tabs.component.ts │ ├── utils │ │ ├── cookies.ts │ │ ├── globals.ts │ │ ├── navigator.d.ts │ │ ├── taxii2lib.ts │ │ └── utils.ts │ └── version-upgrade │ │ ├── version-upgrade.component.html │ │ ├── version-upgrade.component.scss │ │ ├── version-upgrade.component.spec.ts │ │ └── version-upgrade.component.ts ├── assets │ ├── config.json │ ├── icons │ │ ├── MaterialIcons-Regular.ttf │ │ ├── baseline-grid_on-24px.svg │ │ ├── ic_camera_alt_black_24px.svg │ │ ├── ic_check_box_black_24px.svg │ │ ├── ic_check_box_outline_blank_black_24px.svg │ │ ├── ic_clear_black_24px.svg │ │ ├── ic_clear_gray_24px.svg │ │ ├── ic_close_black_24px.svg │ │ ├── ic_color_lens_black_24px.svg │ │ ├── ic_content_copy_black_24px.svg │ │ ├── ic_description_black_24px.svg │ │ ├── ic_done_black_24px.svg │ │ ├── ic_done_gray_24px.svg │ │ ├── ic_exportAllExcel_black_24px.svg │ │ ├── ic_exportAllJson_black_24px.svg │ │ ├── ic_exportJson_black_24px.svg │ │ ├── ic_file_download_black_24px.svg │ │ ├── ic_file_upload_black_24px.svg │ │ ├── ic_file_upload_gray_24px.svg │ │ ├── ic_filter_list_black_24px.svg │ │ ├── ic_format_color_fill_black_24px.svg │ │ ├── ic_format_color_fill_black_nobottom_24px.svg │ │ ├── ic_format_color_fill_gray_nobottom_24px.svg │ │ ├── ic_format_size_black_24px.svg │ │ ├── ic_insert_chart_black_24px.svg │ │ ├── ic_insert_chart_gray_24px.svg │ │ ├── ic_insert_comment_black_24px.svg │ │ ├── ic_insert_comment_gray_24px.svg │ │ ├── ic_keyboard_arrow_down_black_24px.svg │ │ ├── ic_keyboard_arrow_right_black_24px.svg │ │ ├── ic_keyboard_arrow_up_black_24px.svg │ │ ├── ic_layers_clear_black_24px.svg │ │ ├── ic_layers_clear_gray_24px.svg │ │ ├── ic_link_black_24px.svg │ │ ├── ic_link_gray_24px.svg │ │ ├── ic_lock_black_24px.svg │ │ ├── ic_lock_open_black_24px.svg │ │ ├── ic_metadata_black_24px.svg │ │ ├── ic_metadata_gray_24px.svg │ │ ├── ic_palette_black_24px.svg │ │ ├── ic_photo_size_select_large_black_24px.svg │ │ ├── ic_playlist_add_black_24px.svg │ │ ├── ic_push_pin_black_24px.svg │ │ ├── ic_push_pin_gray.svg │ │ ├── ic_remove_circle_black_24px.svg │ │ ├── ic_save_black_24px.svg │ │ ├── ic_save_gray_24px.svg │ │ ├── ic_search_black_24px.svg │ │ ├── ic_search_gray_24px.svg │ │ ├── ic_sort_alphabetically_ascending.svg │ │ ├── ic_sort_alphabetically_ascending_black_24px.svg │ │ ├── ic_sort_alphabetically_black_24px.svg │ │ ├── ic_sort_alphabetically_descending.svg │ │ ├── ic_sort_alphabetically_descending_black_24px.svg │ │ ├── ic_sort_numerically_ascending.svg │ │ ├── ic_sort_numerically_ascending_black_24px.svg │ │ ├── ic_sort_numerically_black_24px.svg │ │ ├── ic_sort_numerically_descending.svg │ │ ├── ic_sort_numerically_descending_black_24px.svg │ │ ├── ic_texture_black_24px.svg │ │ ├── ic_texture_gray_24px.svg │ │ ├── ic_unfold_more_alt.svg │ │ ├── ic_view_large_black_24px.svg │ │ ├── ic_view_list_black_24px.svg │ │ ├── ic_view_list_grey_24px.svg │ │ ├── ic_view_medium_black_24px.svg │ │ ├── ic_view_small_black_24px.svg │ │ ├── ic_visibility_black_24px.svg │ │ ├── ic_visibility_gray_24px.svg │ │ ├── ic_visibility_off_black_24px.svg │ │ ├── table_view_FILL0_wght400_GRAD0_opsz24.svg │ │ ├── unfold_less_black_24px.svg │ │ ├── unfold_less_gray_24px.svg │ │ ├── unfold_more_alt_black_24px.svg │ │ ├── unfold_more_alt_gray_24px.svg │ │ ├── unfold_more_black_24px.svg │ │ └── unfold_more_gray_24px.svg │ └── image_scoreVariableExample.png ├── bootstrap.min.css ├── colors.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── favicon.png ├── index.google-analytics.html ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tests │ └── utils │ │ ├── mock-data.ts │ │ └── mock-layers.ts └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/ 3 | *.spec.ts -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | create_github_release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: ncipollo/release-action@v1 16 | with: 17 | makeLatest: true 18 | name: "attack-navigator ${{github.ref_name}}" 19 | body: "See [the changelog](./CHANGELOG.md) for details about what changed in this release." -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | jobs: 8 | deploy-to-gh-pages: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | defaults: 15 | run: 16 | working-directory: ./nav-app/ 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | cache-dependency-path: nav-app/package-lock.json 30 | 31 | - name: Install Node dependencies 32 | run: npm ci 33 | 34 | - name: Build Navigator 35 | run: npm run build -- --configuration production,googleAnalytics --aot=false --build-optimizer=false --base-href=https://mitre-attack.github.io/attack-navigator/ 36 | 37 | - name: Download and extract Navigator v2 38 | uses: robinraju/release-downloader@v1.10 39 | with: 40 | repository: "mitre-attack/attack-navigator" 41 | tag: "v2.3.2" 42 | fileName: "attack-navigator-v2.zip" 43 | extract: true 44 | 45 | - name: Move v2 to output directory 46 | run: mv ../v2/ dist/ 47 | 48 | - name: Download and extract Navigator v3 49 | uses: robinraju/release-downloader@v1.10 50 | with: 51 | repository: "mitre-attack/attack-navigator" 52 | tag: "v3.1" 53 | fileName: "attack-navigator-v3.zip" 54 | extract: true 55 | 56 | - name: Move v3 to output directory 57 | run: mv ../v3/ dist/ 58 | 59 | - name: Copy redirects 60 | run: cp -r ./redirects/* ./dist/ 61 | 62 | - name: Deploy 63 | uses: peaceiris/actions-gh-pages@v3 64 | if: ${{ github.ref == 'refs/heads/master' }} 65 | with: 66 | deploy_key: ${{ secrets.DEPLOY_KEY }} 67 | publish_dir: ./nav-app/dist 68 | -------------------------------------------------------------------------------- /.github/workflows/run-jasmine-tests.yml: -------------------------------------------------------------------------------- 1 | name: Jasmine Tests 2 | 3 | on: 4 | push: 5 | branches: ["develop"] 6 | pull_request: 7 | branches: ["master", "develop"] 8 | types: [synchronize] 9 | 10 | jobs: 11 | run-tests: 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: ./nav-app/ 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | cache-dependency-path: nav-app/package-lock.json 28 | 29 | - name: Install 30 | run: npm ci 31 | 32 | - name: Run Jasmine tests 33 | run: npm run test -- --code-coverage --no-watch --browsers ChromeHeadlessCI 34 | 35 | - name: Archive code coverage results 36 | uses: actions/upload-artifact@v4 37 | if: always() 38 | with: 39 | name: code-coverage-report 40 | path: nav-app/coverage/chrome/ 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | MD013: false 3 | MD024: 4 | siblings_only: true 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thanks for contributing to `attack-navigator`! 4 | 5 | You are welcome to comment on issues, open new issues, and open pull requests. 6 | 7 | Pull requests should target the **develop** branch of the repository. 8 | 9 | Also, if you contribute any source code, we need you to agree to the following Developer's Certificate of Origin below. 10 | 11 | ## Developer's Certificate of Origin v1.1 12 | 13 | ```text 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | ``` 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | 3 | FROM node:18 4 | 5 | ENV NODE_OPTIONS=--openssl-legacy-provider 6 | 7 | # install node packages - cache for faster future builds 8 | WORKDIR /src/nav-app 9 | COPY ./nav-app/package*.json ./ 10 | 11 | # install packages and build 12 | RUN npm install 13 | 14 | # copy over needed files 15 | COPY ./nav-app/ ./ 16 | 17 | # copy layers directory 18 | WORKDIR /src 19 | COPY layers/ ./layers/ 20 | 21 | # copy markdown files from root 22 | COPY *.md ./ 23 | 24 | WORKDIR /src/nav-app 25 | EXPOSE 4200 26 | 27 | CMD npm start 28 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 The MITRE Corporation 2 | 3 | Approved for Public Release; Distribution Unlimited. Case Number 18-0128. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | This project makes use of ATT&CK® 18 | ATT&CK® Terms of Use - https://attack.mitre.org/resources/terms-of-use/ 19 | -------------------------------------------------------------------------------- /layers/README.md: -------------------------------------------------------------------------------- 1 | # ATT&CK Navigator Layers 2 | 3 | A layer constitutes a set of annotations on the ATT&CK matrix for a specific technology domain. Layers can also store a default configuration of the view such as sorting, visible platforms, and more. The ATT&CK Navigator includes functionalities for exporting annotations into layer files, as well as the ability to import layer files for viewing. 4 | 5 | See the latest [layer format specification](spec/v4.5/layerformat.md) for more information about Layer files. 6 | 7 | ## Sample Layers 8 | 9 | This repository includes a couple of [sample layers](samples/) demonstrating example use cases of layers and the ATT&CK Navigator. The scripts used to generate these layer files can be found in the [mitreattack-python repository](https://github.com/mitre-attack/attack-scripts/tree/master/scripts/layers/samples). These scripts may serve as examples on how to access and work with [ATT&CK data](https://github.com/mitre/cti). 10 | -------------------------------------------------------------------------------- /nav-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /nav-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | /.angular/cache 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /nav-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | dist 5 | node_modules 6 | assets 7 | .angular 8 | e2e -------------------------------------------------------------------------------- /nav-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 150, 7 | "bracketSpacing": true, 8 | "bracketSameLine": true, 9 | "htmlWhitespaceSensitivity": "ignore" 10 | } 11 | -------------------------------------------------------------------------------- /nav-app/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('nav-app App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /nav-app/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /nav-app/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nav-app/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('@angular-devkit/build-angular/plugins/karma'), 10 | require('karma-chrome-launcher'), 11 | require('karma-coverage'), 12 | // require('karma-coverage-istanbul-reporter'), 13 | require('karma-jasmine'), 14 | require('karma-jasmine-html-reporter'), 15 | ], 16 | client: { 17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, 'coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true, 23 | }, 24 | 25 | reporters: ['progress', 'kjhtml', 'coverage'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'], 31 | customLaunchers: { 32 | ChromeHeadlessCI: { 33 | base: 'ChromeHeadless', 34 | flags: ['--no-sandbox'], 35 | }, 36 | }, 37 | singleRun: false, 38 | webpack: { node: { fs: 'empty' } }, // https://github.com/angular/angular-cli/issues/8357 39 | 40 | // For code coverage 41 | files: ['src/**/*.ts'], 42 | preprocessors: { 43 | 'src/**/*.js': ['coverage'], 44 | }, 45 | coverageReporter: { 46 | type: 'html', 47 | dir: 'coverage/', 48 | subdir: 'chrome', 49 | file: 'index.html', 50 | }, 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /nav-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attack-navigator", 3 | "description": "Web app that provides basic navigation and annotation of ATT&CK matrices", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/mitre-attack/attack-navigator.git" 7 | }, 8 | "version": "5.1.0", 9 | "license": "Apache-2.0", 10 | "scripts": { 11 | "ng": "ng", 12 | "start": "ng serve --host 0.0.0.0", 13 | "build": "ng build", 14 | "test": "ng test", 15 | "lint": "ng lint", 16 | "e2e": "ng e2e" 17 | }, 18 | "dependencies": { 19 | "@angular/animations": "^17.3.10", 20 | "@angular/cdk": "^17.3.10", 21 | "@angular/common": "^17.3.10", 22 | "@angular/compiler": "^17.3.10", 23 | "@angular/core": "^17.3.10", 24 | "@angular/forms": "^17.3.10", 25 | "@angular/material": "^17.3.10", 26 | "@angular/platform-browser": "^17.3.10", 27 | "@angular/platform-browser-dynamic": "^17.3.10", 28 | "@angular/router": "^17.3.10", 29 | "@babel/runtime": "^7.22.6", 30 | "@fontsource/roboto": "^5.0.7", 31 | "@fontsource/roboto-mono": "^5.0.7", 32 | "@types/file-saver": "^2.0.1", 33 | "classlist.js": "^1.1.20150312", 34 | "core-js": "^3.31.1", 35 | "d3": "^7.8.5", 36 | "d3-svg-legend": "^2.25.6", 37 | "detect-browser": "^5.3.0", 38 | "file-saver": "^2.0.5", 39 | "load-json-file": "^7.0.1", 40 | "mathjs": "^12.4.2", 41 | "ngx-color-picker": "^16.0.0", 42 | "ngx-drag-drop": "^17.0.0", 43 | "ngx-markdown": "^17.2.1", 44 | "rxjs": "^7.8.1", 45 | "rxjs-compat": "^6.6.7", 46 | "tinycolor2": "^1.6.0", 47 | "tinygradient": "^1.1.5", 48 | "tslib": "^2.5.0", 49 | "zone.js": "~0.14.6" 50 | }, 51 | "devDependencies": { 52 | "@angular-builders/custom-webpack": "^17.0.2", 53 | "@angular-devkit/build-angular": "^17.3.8", 54 | "@angular/cli": "^17.3.8", 55 | "@angular/compiler-cli": "^17.3.10", 56 | "@angular/language-service": "^17.3.10", 57 | "@types/jasmine": "^5.1.4", 58 | "@types/jasminewd2": "^2.0.8", 59 | "@types/node": "^18.15.12", 60 | "codelyzer": "^6.0.0", 61 | "exceljs": "^4.3.0", 62 | "jasmine-core": "^5.1.0", 63 | "jasmine-spec-reporter": "~7.0.0", 64 | "karma": "~6.4.1", 65 | "karma-chrome-launcher": "^3.2.0", 66 | "karma-cli": "~2.0.0", 67 | "karma-coverage": "^2.2.0", 68 | "karma-coverage-istanbul-reporter": "~3.0.2", 69 | "karma-jasmine": "^5.1.0", 70 | "karma-jasmine-html-reporter": "^2.1.0", 71 | "marked": "^12.0.2", 72 | "prettier": "^3.2.5", 73 | "ts-node": "^10.9.1", 74 | "tslint": "~6.1.0", 75 | "typescript": "^5.4.5" 76 | }, 77 | "optionalDependencies": { 78 | "fsevents": "^2.3.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nav-app/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: ['./e2e/**/*.e2e-spec.ts'], 9 | capabilities: { 10 | browserName: 'chrome', 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function () {}, 19 | }, 20 | onPrepare() { 21 | require('ts-node').register({ 22 | project: 'e2e/tsconfig.e2e.json', 23 | }); 24 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /nav-app/redirects/enterprise/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | ATT&CK® Navigator 16 | 17 | 18 |
19 |

ATT&CK Navigator's Enterprise instance has moved to the new multi-domain instance.

20 |

This page should automatically redirect. If it does not, please use the following link:

21 |

https://mitre-attack.github.io/attack-navigator/

22 |
23 | 24 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /nav-app/redirects/mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | ATT&CK® Navigator 16 | 17 | 18 |
19 |

ATT&CK Navigator's Mobile instance has moved to the new multi-domain instance.

20 |

This page should automatically redirect. If it does not, please use the following link:

21 |

https://mitre-attack.github.io/attack-navigator/

22 |
23 | 24 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /nav-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /nav-app/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '../colors.scss'; 2 | 3 | .app-container { 4 | @include adaptive-color('background-color', color(body), color(dark-1)); 5 | height: 100vh; 6 | } 7 | 8 | .nav-app { 9 | @include adaptive-color('background-color', color(light), darken(color(dark), 8%)); 10 | 11 | tabs { 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 13 | font-size: 9pt; 14 | } 15 | 16 | .version-footer { 17 | @include adaptive-color('color', on-color(light), on-color(dark-2)); 18 | font-size: 7pt; 19 | border: none; 20 | background-color: transparent; 21 | text-align: right; 22 | margin-left: 16px; 23 | padding: 0; 24 | display: block; 25 | position: fixed; 26 | bottom: 16px; 27 | &:hover { 28 | text-decoration: underline; 29 | cursor: pointer; 30 | } 31 | @media print { 32 | display: none; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nav-app/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | import { deleteCookie, getCookie, hasCookie, setCookie } from './utils/cookies'; 5 | import { TabsComponent } from './tabs/tabs.component'; 6 | import { MatDialogModule } from '@angular/material/dialog'; 7 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 8 | import { ConfigService } from './services/config.service'; 9 | import { MatTabsModule } from '@angular/material/tabs'; 10 | 11 | describe('AppComponent', () => { 12 | let fixture: ComponentFixture; 13 | let app: any; 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [HttpClientTestingModule, MatDialogModule, MatSnackBarModule, MatTabsModule], 18 | declarations: [AppComponent, TabsComponent], 19 | }).compileComponents(); 20 | 21 | // set up config service 22 | let configService = TestBed.inject(ConfigService); 23 | configService.defaultLayers = { enabled: false }; 24 | 25 | fixture = TestBed.createComponent(AppComponent); 26 | app = fixture.debugElement.componentInstance; 27 | })); 28 | 29 | it('should create the app', waitForAsync(() => { 30 | expect(app).toBeTruthy(); 31 | })); 32 | 33 | it('should intialize title', waitForAsync(() => { 34 | app.ngOnInit(); 35 | let result = app.titleService.getTitle(); 36 | expect(result).toEqual(app.title); 37 | })); 38 | 39 | it(`should have title 'ATT&CK® Navigator'`, waitForAsync(() => { 40 | expect(app.title).toEqual('ATT&CK® Navigator'); 41 | })); 42 | 43 | it('should set user theme to theme-override-dark', waitForAsync(() => { 44 | setCookie('is_user_theme_dark', 'true', 1); 45 | // recreate component 46 | fixture = TestBed.createComponent(AppComponent); 47 | app = fixture.componentInstance; 48 | expect(app.user_theme).toEqual('theme-override-dark'); 49 | })); 50 | 51 | it('should set user theme to theme-override-light', waitForAsync(() => { 52 | setCookie('is_user_theme_dark', 'false', 1); 53 | // recreate component 54 | fixture = TestBed.createComponent(AppComponent); 55 | app = fixture.componentInstance; 56 | expect(app.user_theme).toEqual('theme-override-light'); 57 | })); 58 | 59 | it('should set user theme to theme-use-system', waitForAsync(() => { 60 | deleteCookie('is_user_theme_dark'); 61 | // recreate component 62 | fixture = TestBed.createComponent(AppComponent); 63 | app = fixture.componentInstance; 64 | expect(app.user_theme).toEqual('theme-use-system'); 65 | })); 66 | 67 | it('should handle dark theme change', waitForAsync(() => { 68 | app.themeChangeHandler('dark'); 69 | expect(app.user_theme).toEqual('theme-override-dark'); 70 | expect(hasCookie('is_user_theme_dark')).toBeTrue(); 71 | expect(getCookie('is_user_theme_dark')).toEqual('true'); 72 | })); 73 | 74 | it('should handle light theme change', waitForAsync(() => { 75 | app.themeChangeHandler('light'); 76 | expect(app.user_theme).toEqual('theme-override-light'); 77 | expect(hasCookie('is_user_theme_dark')).toBeTrue(); 78 | expect(getCookie('is_user_theme_dark')).toEqual('false'); 79 | })); 80 | 81 | it('should handle system theme change', waitForAsync(() => { 82 | setCookie('is_user_theme_dark', 'true', 1); 83 | 84 | app.themeChangeHandler('system'); 85 | expect(app.user_theme).toEqual('theme-use-system'); 86 | expect(hasCookie('is_user_theme_dark')).toBeFalse(); 87 | })); 88 | 89 | it('should prompt to navigate away', waitForAsync(() => { 90 | app.configService.setFeature('leave_site_dialog', true); 91 | let prompt = 'Are you sure you want to navigate away? Your data may be lost!'; 92 | let event$ = { returnValue: null }; 93 | app.promptNavAway(event$); 94 | expect(event$.returnValue).toEqual(prompt); 95 | })); 96 | 97 | it('should not prompt to navigate away', waitForAsync(() => { 98 | app.configService.setFeature('leave_site_dialog', false); 99 | let event$ = { returnValue: null }; 100 | app.promptNavAway(event$); 101 | expect(event$.returnValue).toEqual(null); 102 | })); 103 | }); 104 | -------------------------------------------------------------------------------- /nav-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, HostListener, OnInit } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { TabsComponent } from './tabs/tabs.component'; 4 | import { ConfigService } from './services/config.service'; 5 | import * as globals from './utils/globals'; 6 | import { IconsService } from './services/icons.service'; 7 | import { deleteCookie, getCookie, hasCookie, setCookie } from './utils/cookies'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrls: ['./app.component.scss'], 13 | }) 14 | export class AppComponent implements OnInit { 15 | @ViewChild(TabsComponent) tabsComponent; 16 | 17 | navVersion: string = globals.navVersion; 18 | public user_theme: string; 19 | title = 'ATT&CK® Navigator'; 20 | 21 | @HostListener('window:beforeunload', ['$event']) 22 | promptNavAway($event) { 23 | if (!this.configService.getFeature('leave_site_dialog')) return; 24 | //this text only shows in the data, not visible to user as far as I can tell 25 | //however, if it's not included the window doesn't open. 26 | $event.returnValue = 'Are you sure you want to navigate away? Your data may be lost!'; 27 | } 28 | 29 | constructor( 30 | public configService: ConfigService, 31 | private iconsService: IconsService, 32 | public titleService: Title 33 | ) { 34 | Array.prototype.includes = function (value): boolean { 35 | for (let i = 0; i < this.length; i++) { 36 | if (this[i] === value) return true; 37 | } 38 | return false; 39 | }; 40 | if (hasCookie('is_user_theme_dark') && getCookie('is_user_theme_dark') === 'true') { 41 | this.user_theme = 'theme-override-dark'; 42 | } else if (getCookie('is_user_theme_dark') === 'false') { 43 | this.user_theme = 'theme-override-light'; 44 | } else { 45 | this.user_theme = 'theme-use-system'; 46 | } 47 | } 48 | 49 | ngOnInit() { 50 | this.iconsService.registerIcons(); 51 | this.titleService.setTitle(this.title); 52 | } 53 | 54 | themeChangeHandler(theme: string) { 55 | if (theme === 'system') { 56 | if (hasCookie('is_user_theme_dark')) deleteCookie('is_user_theme_dark'); 57 | this.user_theme = 'theme-use-system'; 58 | } else { 59 | this.user_theme = theme === 'dark' ? 'theme-override-dark' : 'theme-override-light'; 60 | setCookie('is_user_theme_dark', theme === 'dark' ? 'true' : 'false', 180); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /nav-app/src/app/changelog/changelog.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Changelog

3 | 4 | 5 | 6 | 7 | 8 | 9 |
-------------------------------------------------------------------------------- /nav-app/src/app/changelog/changelog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from "@angular/core/testing"; 2 | import { ChangelogComponent } from "./changelog.component" 3 | import { MarkdownModule, MarkdownService } from "ngx-markdown"; 4 | import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog"; 5 | import { NO_ERRORS_SCHEMA } from "@angular/core"; 6 | import { HttpClient, HttpClientModule } from "@angular/common/http"; 7 | 8 | describe('ChangelogComponent', () => { 9 | let component: ChangelogComponent; 10 | let fixture: ComponentFixture; 11 | let markdownService: MarkdownService; 12 | 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | declarations: [ChangelogComponent], 16 | imports: [ 17 | MatDialogModule, 18 | MarkdownModule.forRoot({ loader: HttpClient }), 19 | HttpClientModule 20 | ], 21 | providers: [ 22 | {provide: MAT_DIALOG_DATA, useValue: {someData: 'test data'}}, 23 | {provide: MatDialogRef, useValue: {}}, 24 | MarkdownService 25 | ], 26 | schemas: [NO_ERRORS_SCHEMA] 27 | }).compileComponents(); 28 | 29 | fixture = TestBed.createComponent(ChangelogComponent); 30 | component = fixture.componentInstance; 31 | markdownService = TestBed.inject(MarkdownService); 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | 39 | it('should inject MAT_DIALOG_DATA', () => { 40 | expect(component.data).toEqual({someData: 'test data'}); 41 | }); 42 | 43 | it('should inject MarkdownService', () => { 44 | expect(markdownService).toBeTruthy(); 45 | }); 46 | 47 | it('should inject MatDialog', () => { 48 | expect(component['dialog']).toBeTruthy(); 49 | }); 50 | }) -------------------------------------------------------------------------------- /nav-app/src/app/changelog/changelog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, ViewChild } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; 3 | import { MarkdownComponent, MarkdownService } from 'ngx-markdown'; 4 | 5 | @Component({ 6 | selector: 'app-changelog', 7 | templateUrl: './changelog.component.html', 8 | }) 9 | export class ChangelogComponent { 10 | @ViewChild('markdownElement', { static: false }) public markdownElement: any; 11 | 12 | constructor( 13 | private dialog: MatDialog, 14 | private markdownService: MarkdownService, 15 | @Inject(MAT_DIALOG_DATA) public data 16 | ) { 17 | // intentionally left blank 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/context-menu-item.ts: -------------------------------------------------------------------------------- 1 | import { Tactic } from './stix/tactic'; 2 | import { Technique } from './stix/technique'; 3 | 4 | export class ContextMenuItem { 5 | public readonly label: string; 6 | private readonly url: string; 7 | private readonly subtechnique_url: string; 8 | 9 | constructor(label, url, subtechnique_url = null) { 10 | this.label = label; 11 | this.url = url; 12 | this.subtechnique_url = subtechnique_url; 13 | } 14 | 15 | public getReplacedURL(technique: Technique, tactic: Tactic): string { 16 | if (this.subtechnique_url && technique.isSubtechnique) { 17 | return this.subtechnique_url 18 | .replace(/{{parent_technique_attackID}}/g, technique.parent.attackID) 19 | .replace(/{{parent_technique_stixID}}/g, technique.parent.id) 20 | .replace(/{{parent_technique_name}}/g, technique.parent.name.replace(/ /g, '-').toLowerCase()) 21 | 22 | .replace(/{{subtechnique_attackID}}/g, technique.attackID) 23 | .replace(/{{subtechnique_attackID_suffix}}/g, technique.attackID.split('.')[1]) 24 | .replace(/{{subtechnique_stixID}}/g, technique.id) 25 | .replace(/{{subtechnique_name}}/g, technique.name.replace(/ /g, '-').toLowerCase()) 26 | 27 | .replace(/{{tactic_attackID}}/g, tactic.attackID) 28 | .replace(/{{tactic_stixID}}/g, tactic.id) 29 | .replace(/{{tactic_name}}/g, tactic.shortname); 30 | } else { 31 | return this.url 32 | .replace(/{{technique_attackID}}/g, technique.attackID) 33 | .replace(/{{technique_stixID}}/g, technique.id) 34 | .replace(/{{technique_name}}/g, technique.name.replace(/ /g, '-').toLowerCase()) 35 | 36 | .replace(/{{tactic_attackID}}/g, tactic.attackID) 37 | .replace(/{{tactic_stixID}}/g, tactic.id) 38 | .replace(/{{tactic_name}}/g, tactic.shortname); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/domain.ts: -------------------------------------------------------------------------------- 1 | import { ServiceAuth } from '../services/data.service'; 2 | import { Campaign } from './stix/campaign'; 3 | import { DataComponent } from './stix/data-component'; 4 | import { Group } from './stix/group'; 5 | import { Matrix } from './stix/matrix'; 6 | import { Mitigation } from './stix/mitigation'; 7 | import { Note } from './stix/note'; 8 | import { Software } from './stix/software'; 9 | import { Tactic } from './stix/tactic'; 10 | import { Technique } from './stix/technique'; 11 | import { Version } from './version'; 12 | import { Asset } from './stix/asset'; 13 | 14 | export class Domain { 15 | public readonly id: string; // domain ID 16 | public readonly domain_identifier: string; //domain ID without the version suffix 17 | public readonly name: string; // domain display name 18 | public readonly version: Version; // ATT&CK version 19 | 20 | public urls: string[] = []; 21 | public taxii_url: string = ''; 22 | public taxii_collection: string = ''; 23 | public authentication: ServiceAuth; 24 | public dataLoaded: boolean = false; 25 | public dataLoadedCallbacks: any[] = []; 26 | 27 | // this should only be enabled if the user loads custom data via URL 28 | public isCustom: boolean = false; 29 | 30 | public matrices: Matrix[] = []; 31 | public get tactics(): Tactic[] { 32 | let tactics = []; 33 | for (let matrix of this.matrices) { 34 | tactics = tactics.concat(matrix.tactics); 35 | } 36 | return tactics; 37 | } 38 | public techniques: Technique[] = []; 39 | public platforms: string[] = []; // platforms defined on techniques and software of the domain 40 | public subtechniques: Technique[] = []; 41 | public software: Software[] = []; 42 | public campaigns: Campaign[] = []; 43 | public assets: Asset[] = []; 44 | public dataComponents: DataComponent[] = []; 45 | public dataSources = new Map(); // Map data source ID to name and urls to be used by data components 46 | public groups: Group[] = []; 47 | public mitigations: Mitigation[] = []; 48 | public notes: Note[] = []; 49 | public relationships: any = { 50 | // subtechnique subtechnique-of technique 51 | // ID of technique to [] of subtechnique IDs 52 | subtechniques_of: new Map(), 53 | // data component related to technique 54 | // ID of data component to [] of technique IDs 55 | component_rel: new Map(), 56 | // group uses technique 57 | // ID of group to [] of technique IDs 58 | group_uses: new Map(), 59 | // software uses technique 60 | // ID of software to [] of technique IDs 61 | software_uses: new Map(), 62 | // campaign uses technique 63 | // ID of campaign to [] of technique IDs 64 | campaign_uses: new Map(), 65 | // campaigns attributed to group 66 | // ID of group to [] of campaign IDs 67 | campaigns_attributed_to: new Map(), 68 | // mitigation mitigates technique 69 | // ID of mitigation to [] of technique IDs 70 | mitigates: new Map(), 71 | // object is revoked-by object 72 | // ID of object to ID of revoking object 73 | revoked_by: new Map(), 74 | // technique targets asset 75 | // ID of asset to [] of technique IDs 76 | targeted_assets: new Map(), 77 | }; 78 | 79 | constructor(domain_identifier: string, name: string, version: Version, urls?: string[]) { 80 | this.id = `${domain_identifier}-${version.number}`; 81 | this.domain_identifier = domain_identifier; 82 | this.name = name; 83 | this.version = version; 84 | if (urls) this.urls = urls; 85 | } 86 | 87 | /** 88 | * Get the version number for this domain 89 | */ 90 | public getVersion(): string { 91 | return this.version.number; 92 | } 93 | 94 | public executeCallbacks(): void { 95 | for (let callback of this.dataLoadedCallbacks) { 96 | callback(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/filter.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from './domain'; 2 | 3 | export class Filter { 4 | // the data for a specific filter 5 | private readonly domain: string; 6 | public platforms: { 7 | options: string[]; 8 | selection: string[]; 9 | }; 10 | 11 | constructor() { 12 | this.platforms = { 13 | selection: [], 14 | options: [], 15 | }; 16 | } 17 | 18 | /** 19 | * Initialize the platform options according to the data in the domain 20 | * @param {Domain} domain the domain to parse for platform options 21 | */ 22 | public initPlatformOptions(domain: Domain): void { 23 | this.platforms.options = JSON.parse(JSON.stringify(domain.platforms)); 24 | if (!this.platforms.selection.length) { 25 | // prevent overwriting current selection 26 | this.platforms.selection = JSON.parse(JSON.stringify(domain.platforms)); 27 | } 28 | } 29 | 30 | /** 31 | * toggle the given value in the given filter 32 | * @param {*} filterName the name of the filter 33 | * @param {*} value the value to toggle 34 | */ 35 | public toggleInFilter(filterName: string, value: string): void { 36 | if (!this[filterName].options.includes(value)) { 37 | console.error('not a valid option to toggle', value, this[filterName]); 38 | return; 39 | } 40 | if (this[filterName].selection.includes(value)) { 41 | let index = this[filterName].selection.indexOf(value); 42 | this[filterName].selection.splice(index, 1); 43 | } else { 44 | this[filterName].selection.push(value); 45 | } 46 | } 47 | 48 | /** 49 | * determine if the given value is active in the filter 50 | * @param {*} filterName the name of the filter 51 | * @param {*} value the value to determine 52 | * @returns {boolean} true if value is currently enabled in the filter 53 | */ 54 | public inFilter(filterName, value): boolean { 55 | return this[filterName].selection.includes(value); 56 | } 57 | 58 | /** 59 | * Return the string representation of this filter 60 | * @return stringified filter 61 | */ 62 | public serialize(): string { 63 | return JSON.stringify({ platforms: this.platforms.selection }); 64 | } 65 | 66 | /** 67 | * Replace the properties of this object with those of the given serialized filter 68 | * @param rep filter object 69 | */ 70 | public deserialize(rep: any): void { 71 | let isStringArray = function (arr): boolean { 72 | for (let item of arr) { 73 | if (typeof item !== 'string') { 74 | console.error('TypeError:', item, '(', typeof item, ')', 'is not a string'); 75 | return false; 76 | } 77 | } 78 | return true; 79 | }; 80 | 81 | if (rep.platforms) { 82 | if (isStringArray(rep.platforms)) { 83 | let backwards_compatibility_mappings = { 84 | //backwards compatibility with older layers 85 | android: 'Android', 86 | ios: 'iOS', 87 | 88 | windows: 'Windows', 89 | linux: 'Linux', 90 | mac: 'macOS', 91 | 92 | AWS: 'IaaS', 93 | GCP: 'IaaS', 94 | Azure: 'IaaS', 95 | }; 96 | const selection = new Set(); 97 | rep.platforms.forEach(function (platform) { 98 | if (platform in backwards_compatibility_mappings) selection.add(backwards_compatibility_mappings[platform]); 99 | else selection.add(platform); 100 | }); 101 | this.platforms.selection = Array.from(selection); 102 | } else console.error('TypeError: filter platforms field is not a string[]'); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/index.ts: -------------------------------------------------------------------------------- 1 | export { VersionChangelog } from './version-changelog'; 2 | export { Version } from './version'; 3 | export { Domain } from './domain'; 4 | export { ContextMenuItem } from './context-menu-item'; 5 | export { Gradient, Gcolor } from './gradient'; 6 | export { Filter } from './filter'; 7 | export { LayoutOptions } from './layout-options'; 8 | export { Link } from './link'; 9 | export { Metadata } from './metadata'; 10 | export { TechniqueVM } from './technique-vm'; 11 | export { ViewModel } from './view-model'; 12 | export { Tab } from './tab'; 13 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/link.ts: -------------------------------------------------------------------------------- 1 | export class Link { 2 | public label: string; 3 | public url: string; 4 | public divider: boolean; 5 | 6 | constructor() { 7 | // intentionally left blank 8 | } 9 | 10 | public serialize(): object { 11 | return this.label && this.url ? { label: this.label, url: this.url } : { divider: this.divider }; 12 | } 13 | 14 | public deserialize(rep: any): void { 15 | let obj = typeof rep == 'string' ? JSON.parse(rep) : rep; 16 | if ('url' in obj) { 17 | // label & url object 18 | if (typeof obj.url === 'string') this.url = obj.url; 19 | else console.error("TypeError: Link field 'url' is not a string"); 20 | 21 | if ('label' in obj) { 22 | if (typeof obj.label === 'string') this.label = obj.label; 23 | else console.error("TypeError: Link field 'label' is not a string"); 24 | } else console.error("Error: Link required field 'label' not present"); 25 | } else if ('divider' in obj) { 26 | // divider object 27 | if (typeof obj.divider === 'boolean') this.divider = obj.divider; 28 | else console.error("TypeError: Link field 'divider' is not a boolean"); 29 | } else console.error("Error: Link required field 'url' or 'divider' not present"); 30 | } 31 | 32 | public valid(): boolean { 33 | return (this.label && this.label.length > 0 && this.url && this.url.length > 0) || this.divider !== undefined; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/metadata.ts: -------------------------------------------------------------------------------- 1 | export class Metadata { 2 | public name: string; 3 | public value: string; 4 | public divider: boolean; 5 | 6 | public serialize(): object { 7 | return this.name && this.value ? { name: this.name, value: this.value } : { divider: this.divider }; 8 | } 9 | 10 | public deserialize(rep: any): void { 11 | let obj = typeof rep == 'string' ? JSON.parse(rep) : rep; 12 | 13 | if ('name' in obj) { 14 | // name & value object 15 | if (typeof obj.name === 'string') this.name = obj.name; 16 | else console.error("TypeError: Metadata field 'name' is not a string"); 17 | 18 | if ('value' in obj) { 19 | if (typeof obj.value === 'string') this.value = obj.value; 20 | else console.error("TypeError: Metadata field 'value' is not a string"); 21 | } else console.error("Error: Metadata required field 'value' not present"); 22 | } else if ('divider' in obj) { 23 | // divider object 24 | if (typeof obj.divider === 'boolean') this.divider = obj.divider; 25 | else console.error("TypeError: Metadata field 'divider' is not a boolean"); 26 | } else console.error("Error: Metadata required field 'name' or 'divider' not present"); 27 | } 28 | 29 | public valid(): boolean { 30 | return (this.name && this.name.length > 0 && this.value && this.value.length > 0) || this.divider !== undefined; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/asset.ts: -------------------------------------------------------------------------------- 1 | import { StixObject } from './stix-object'; 2 | 3 | export class Asset extends StixObject { 4 | /** 5 | * Get techniques targeting this asset 6 | * @returns {string[]} technique IDs targeting this asset 7 | */ 8 | public targeted(domainVersionID): string[] { 9 | let rels = this.dataService.getDomain(domainVersionID).relationships.targeted_assets; 10 | if (rels.has(this.id)) { 11 | return rels.get(this.id); 12 | } else { 13 | return []; 14 | } 15 | } 16 | 17 | /** 18 | * Get all techniques related to the asset 19 | */ 20 | public relatedTechniques(domainVersionID): string[] { 21 | return this.targeted(domainVersionID); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/campaign.ts: -------------------------------------------------------------------------------- 1 | import { StixObject } from './stix-object'; 2 | 3 | export class Campaign extends StixObject { 4 | /** 5 | * Get techniques used by the campaign 6 | * @param domainVersionID the ID of the domain and version 7 | * @returns {string[]} technique IDs used by the campaign 8 | */ 9 | public used(domainVersionID): string[] { 10 | let relationships = this.dataService.getDomain(domainVersionID).relationships.campaign_uses; 11 | if (relationships.has(this.id)) { 12 | return relationships.get(this.id); 13 | } else { 14 | return []; 15 | } 16 | } 17 | 18 | /** 19 | * Get all techniques related to the campaign 20 | * @param domainVersionID the ID of the domain and version 21 | * @returns {string[]} technique IDs used by the campaign 22 | */ 23 | public relatedTechniques(domainVersionID): string[] { 24 | return this.used(domainVersionID); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/data-component.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { StixObject } from './stix-object'; 3 | import { Technique } from './technique'; 4 | 5 | export class DataComponent extends StixObject { 6 | public readonly url: string; 7 | public readonly dataSource: string; 8 | 9 | constructor(stixSDO: any, dataService: DataService) { 10 | super(stixSDO, dataService, false); 11 | this.dataSource = stixSDO.x_mitre_data_source_ref; 12 | } 13 | 14 | /** 15 | * Get techniques related to the data component 16 | * @param domainVersionID the ID of the domain and version 17 | * @returns {Technique[]} list of techniques used by the data component 18 | */ 19 | public techniques(domainVersionID): Technique[] { 20 | const techniques = []; 21 | const domain = this.dataService.getDomain(domainVersionID); 22 | 23 | let relationships = domain.relationships.component_rel; 24 | if (relationships.has(this.id)) { 25 | relationships.get(this.id).forEach((targetID) => { 26 | const technique = domain.techniques.find((t) => t.id === targetID); 27 | if (technique) techniques.push(technique); 28 | }); 29 | } 30 | return techniques; 31 | } 32 | /** 33 | * Get the data source related to this data component 34 | * @param domainVersionID the ID of the domain and version 35 | * @returns { name: string, url: string } the name and first url of the data source referenced by this data component 36 | */ 37 | public source(domainVersionID) { 38 | const dataSources = this.dataService.getDomain(domainVersionID).dataSources; 39 | if (dataSources.has(this.dataSource)) { 40 | const source = dataSources.get(this.dataSource); 41 | let url = ''; 42 | if (source.external_references && source.external_references[0] && source.external_references[0].url) 43 | url = source.external_references[0].url; 44 | return { name: source.name, url: url }; 45 | } else return { name: '', url: '' }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/group.ts: -------------------------------------------------------------------------------- 1 | import { StixObject } from './stix-object'; 2 | 3 | export class Group extends StixObject { 4 | /** 5 | * Get the techniques used by this group 6 | * @param domainVersionID the ID of the domain and version 7 | * @returns {string[]} technique IDs used by this group 8 | */ 9 | public used(domainVersionID): string[] { 10 | let rels = this.dataService.getDomain(domainVersionID).relationships.group_uses; 11 | if (rels.has(this.id)) { 12 | return rels.get(this.id); 13 | } else { 14 | return []; 15 | } 16 | } 17 | 18 | /** 19 | * Get techniques used by campaigns attributed to this group 20 | * @param domainVersionID the ID of the domain and version 21 | * @returns {string[]} technique IDs used by campaigns attributed to this group 22 | */ 23 | public campaignsUsed(domainVersionID): string[] { 24 | // get campaigns attributed to groups 25 | let attributedCampaigns = this.dataService.getDomain(domainVersionID).relationships.campaigns_attributed_to; 26 | // get techniques used by campaigns 27 | let rels = this.dataService.getDomain(domainVersionID).relationships.campaign_uses; 28 | if (attributedCampaigns.has(this.id)) { 29 | // get set of techniques used by attributed campaigns 30 | let techniques = []; 31 | attributedCampaigns.get(this.id).forEach((campaign_id) => { 32 | if (rels.has(campaign_id)) techniques = techniques.concat(rels.get(campaign_id)); 33 | }); 34 | return techniques; 35 | } else return []; // no attributed campaigns 36 | } 37 | 38 | /** 39 | * Get all techniques related to the group 40 | */ 41 | public relatedTechniques(domainVersionID): string[] { 42 | let usedSet = new Set(this.used(domainVersionID).concat(this.campaignsUsed(domainVersionID))); 43 | return Array.from(usedSet); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/index.ts: -------------------------------------------------------------------------------- 1 | export { StixObject } from './stix-object'; 2 | export { Campaign } from './campaign'; 3 | export { DataComponent } from './data-component'; 4 | export { Group } from './group'; 5 | export { Matrix } from './matrix'; 6 | export { Mitigation } from './mitigation'; 7 | export { Note } from './note'; 8 | export { Software } from './software'; 9 | export { Tactic } from './tactic'; 10 | export { Technique } from './technique'; 11 | export { Asset } from './asset'; 12 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/matrix.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { StixObject } from './stix-object'; 3 | import { Tactic } from './tactic'; 4 | import { Technique } from './technique'; 5 | 6 | export class Matrix extends StixObject { 7 | public readonly tactics: Tactic[]; //tactics found under this Matrix 8 | 9 | /** 10 | * Creates an instance of Matrix. 11 | * @param {*} stixSDO for the matrix 12 | * @param {Map} idToTacticSDO map of tactic ID to tactic SDO 13 | * @param {Technique[]} techniques all techniques defined in the domain 14 | */ 15 | constructor(stixSDO: any, idToTacticSDO: Map, techniques: Technique[], dataService: DataService) { 16 | super(stixSDO, dataService); 17 | this.tactics = stixSDO.tactic_refs 18 | .map((tacticID) => idToTacticSDO.get(tacticID)) // Get tacticSDOs 19 | .filter((tacticSDO) => tacticSDO) // Filter out nulls (tacticSDO not found) 20 | .map((tacticSDO) => new Tactic(tacticSDO, techniques, this.dataService)); // Create Tactic objects 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/mitigation.ts: -------------------------------------------------------------------------------- 1 | import { StixObject } from './stix-object'; 2 | 3 | export class Mitigation extends StixObject { 4 | /** 5 | * Get techniques mitigated by this mitigation 6 | * @returns {string[]} technique IDs mitigated by this mitigation 7 | */ 8 | public mitigated(domainVersionID): string[] { 9 | let rels = this.dataService.getDomain(domainVersionID).relationships.mitigates; 10 | if (rels.has(this.id)) { 11 | return rels.get(this.id); 12 | } else { 13 | return []; 14 | } 15 | } 16 | 17 | /** 18 | * Get all techniques related to the mitigation 19 | */ 20 | public relatedTechniques(domainVersionID): string[] { 21 | return this.mitigated(domainVersionID); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/note.ts: -------------------------------------------------------------------------------- 1 | export class Note { 2 | public readonly abstract?: string; // brief summary of note content 3 | public readonly content: string; // content of the note 4 | public readonly object_refs: string[]; // list of STIX objects the note is applied to 5 | 6 | /** 7 | * Creates an instance of Note. 8 | * @param {any} stixSDO for the note 9 | */ 10 | constructor(stixSDO: any) { 11 | if (stixSDO.abstract) this.abstract = stixSDO.abstract; 12 | this.content = stixSDO.content; 13 | this.object_refs = stixSDO.object_refs; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/software.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { StixObject } from './stix-object'; 3 | 4 | export class Software extends StixObject { 5 | public readonly platforms: string[] = []; //platforms for this software 6 | 7 | /** 8 | * Creates an instance of Software. 9 | * @param {any} stixSDO for the software 10 | * @param {DataService} DataService the software occurs within 11 | */ 12 | constructor(stixSDO: any, dataService: DataService) { 13 | super(stixSDO, dataService); 14 | this.platforms = stixSDO.x_mitre_platforms ? stixSDO.x_mitre_platforms.map((platform) => platform.trim()) : undefined; 15 | } 16 | 17 | /** 18 | * Get techniques used by this software 19 | * @param domainVersionID the ID of the domain and version 20 | * @returns {string[]} technique IDs used by this software 21 | */ 22 | public used(domainVersionID): string[] { 23 | let rels = this.dataService.getDomain(domainVersionID).relationships.software_uses; 24 | if (rels.has(this.id)) { 25 | return rels.get(this.id); 26 | } else { 27 | return []; 28 | } 29 | } 30 | /** 31 | * Get all techniques related to the software 32 | */ 33 | public relatedTechniques(domainVersionID): string[] { 34 | return this.used(domainVersionID); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/stix-object.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | 3 | export abstract class StixObject { 4 | public readonly id: string; // STIX ID 5 | public readonly attackID: string; // ATT&CK ID 6 | public readonly name: string; // name of object 7 | public readonly description: string; // description of object 8 | public readonly url: string; // URL of object on the ATT&CK website 9 | public readonly created: string; // date object was created 10 | public readonly modified: string; // date object was last modified 11 | public readonly revoked: boolean; // is the object revoked? 12 | public readonly deprecated: boolean; // is the object deprecated? 13 | public readonly version: string; // object version 14 | protected readonly dataService: DataService; 15 | 16 | constructor(stixSDO: any, dataService: DataService, supportsAttackID = true) { 17 | // Properties 18 | this.id = stixSDO.id; 19 | this.name = stixSDO.name; 20 | this.description = stixSDO.description; 21 | this.created = stixSDO.created; 22 | this.modified = stixSDO.modified; 23 | this.revoked = stixSDO.revoked ? stixSDO.revoked : false; 24 | this.deprecated = stixSDO.x_mitre_deprecated ? stixSDO.x_mitre_deprecated : false; 25 | this.version = stixSDO.x_mitre_version ? stixSDO.x_mitre_version : ''; 26 | this.dataService = dataService; 27 | 28 | // ATT&CK ID 29 | if (supportsAttackID) { 30 | if (stixSDO.external_references && stixSDO.external_references[0] && stixSDO.external_references[0].external_id) { 31 | this.attackID = stixSDO.external_references[0].external_id; 32 | } else { 33 | alert('Error: external_references has invalid format in imported StixObject (ID: ' + stixSDO.id + ')'); 34 | throw new Error( 35 | 'Error: external_references has invalid format in imported StixObject. Read more here: https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html#_72bcfr3t79jx' 36 | ); 37 | } 38 | } 39 | 40 | // URL 41 | if ('external_references' in stixSDO && stixSDO.external_references.length > 0) { 42 | this.url = stixSDO.external_references[0].url; 43 | } else { 44 | this.url = ''; 45 | } 46 | } 47 | 48 | /** 49 | * Compare this object's version number to another object's version number 50 | * @param that the object to compare to 51 | * @returns 0 if the objects have the same version, 52 | * > 0 if this object's version is greater, 53 | * < 0 if that object's version is greater 54 | */ 55 | public compareVersion(that: StixObject): number { 56 | if (!this.version || !that.version) return 0; // one or both of the objects have no version 57 | 58 | let thisVersion = this.version.split('.'); 59 | let thatVersion = that.version.split('.'); 60 | 61 | for (let i = 0; i < Math.max(thisVersion.length, thatVersion.length); i++) { 62 | if (thisVersion.length == thatVersion.length && thisVersion.length < i) return 0; 63 | if (thisVersion.length < i) return -1; 64 | if (thatVersion.length < i) return 1; 65 | if (+thisVersion[i] == +thatVersion[i]) continue; 66 | return +thisVersion[i] - +thatVersion[i]; 67 | } 68 | return 0; 69 | } 70 | 71 | /** 72 | * get the stix object that this object is revoked by 73 | * @param {string} domainVersionID the ID of the domain & version this object is found in 74 | * @returns {string} object ID this object is revoked by 75 | */ 76 | public revoked_by(domainVersionID): string { 77 | let rels = this.dataService.getDomain(domainVersionID).relationships.revoked_by; 78 | if (rels.has(this.id)) return rels.get(this.id); 79 | else return undefined; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/tactic.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { StixObject } from './stix-object'; 3 | import { Technique } from './technique'; 4 | 5 | export class Tactic extends StixObject { 6 | public readonly techniques: Technique[]; // techniques found under this tactic 7 | public readonly shortname: string; // shortname property, AKA phase-name for techniques' kill-chain phases 8 | 9 | /** 10 | * Creates an instance of Tactic. 11 | * @param {any} stixSDO for the tactic 12 | * @param {Technique[]} techniques all techniques in the domain 13 | */ 14 | constructor(stixSDO: any, techniques: Technique[], dataService: DataService) { 15 | super(stixSDO, dataService); 16 | this.shortname = stixSDO.x_mitre_shortname; 17 | this.techniques = techniques.filter((technique: Technique) => { 18 | if (!technique.revoked && !technique.deprecated) return technique.tactics.includes(this.shortname); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/stix/technique.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { StixObject } from './stix-object'; 3 | import { Tactic } from './tactic'; 4 | 5 | export class Technique extends StixObject { 6 | public readonly platforms: string[]; // platforms for this technique. 7 | public readonly tactics: string[]; // tactics this technique is found under in phase-name format 8 | public readonly subtechniques: Technique[]; // subtechniques under this technique 9 | public readonly datasources: string; // data sources of the technique 10 | public parent: Technique = null; // parent technique. Only present if it's a sub-technique 11 | public get isSubtechnique() { 12 | return this.parent != null; 13 | } 14 | 15 | /** 16 | * Creates an instance of Technique. 17 | * @param {any} stixSDO for the technique 18 | * @param {Technique[]} subtechniques occuring under the technique 19 | */ 20 | constructor(stixSDO: any, subtechniques: Technique[], dataService: DataService) { 21 | super(stixSDO, dataService); 22 | this.platforms = stixSDO.x_mitre_platforms ? stixSDO.x_mitre_platforms.map((platform) => platform.trim()) : undefined; 23 | this.datasources = stixSDO.x_mitre_data_sources ? stixSDO.x_mitre_data_sources.toString() : ''; 24 | if (!this.revoked && !this.deprecated) { 25 | this.tactics = stixSDO.kill_chain_phases.map((phase) => phase.phase_name); 26 | } 27 | 28 | this.subtechniques = subtechniques.filter((sub) => !(sub.deprecated || sub.revoked)); 29 | for (let subtechnique of this.subtechniques) { 30 | subtechnique.parent = this; 31 | } 32 | } 33 | 34 | /** 35 | * Get an ID identifying this technique under a specific tactic 36 | * @param {string|Tactic} tactic tactic name in phase-name/shortname format, or a Tactic object itself 37 | * @returns {string} ID for this technique under that tactic 38 | */ 39 | public get_technique_tactic_id(tactic: string | Tactic): string { 40 | let tactic_shortname = tactic instanceof Tactic ? tactic.shortname : tactic; 41 | 42 | if (!this.tactics.includes(tactic_shortname)) { 43 | throw new Error(tactic_shortname + ' is not a tactic of ' + this.attackID); 44 | } 45 | return this.attackID + '^' + tactic_shortname; 46 | } 47 | 48 | /** 49 | * Get all possible IDs identifying this technique under tactics 50 | * Basically the same as calling get_technique_tactic_id with all valid tactic values 51 | */ 52 | public get_all_technique_tactic_ids(): string[] { 53 | if (this.revoked || this.deprecated) return []; 54 | return this.tactics.map((shortname: string) => this.get_technique_tactic_id(shortname)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/tab.ts: -------------------------------------------------------------------------------- 1 | import { ViewModel } from './view-model'; 2 | 3 | export class Tab { 4 | public title: string; 5 | public viewModel: ViewModel; 6 | public domain: string = ''; 7 | public isDataTable: boolean; 8 | public isCloseable: boolean = false; 9 | public showScoreVariables: boolean = false; 10 | 11 | constructor(title: string, isCloseable: boolean, showScoreVariables: boolean, domain: string, isDataTable: boolean) { 12 | this.title = title; 13 | this.isCloseable = isCloseable; 14 | this.showScoreVariables = showScoreVariables; 15 | this.domain = domain; 16 | this.isDataTable = isDataTable; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/version-changelog.ts: -------------------------------------------------------------------------------- 1 | export class VersionChangelog { 2 | public oldDomainVersionID: string; 3 | public newDomainVersionID: string; 4 | public additions: string[] = []; // new objects added to newest version 5 | public changes: string[] = []; // object changes between versions 6 | public minor_changes: string[] = []; // changes to objects without version increments 7 | public deprecations: string[] = []; // objects deprecated since older version 8 | public revocations: string[] = []; // objects revoked since older version 9 | public unchanged: string[] = []; // objects which have not changed between versions 10 | 11 | public reviewed = new Set(); 12 | public copied = new Set(); 13 | 14 | constructor(oldDomainVersionID: string, newDomainVersionID: string) { 15 | this.oldDomainVersionID = oldDomainVersionID; 16 | this.newDomainVersionID = newDomainVersionID; 17 | } 18 | 19 | /** Get the length of the version changelog */ 20 | public length(): number { 21 | return ( 22 | this.additions.length + 23 | this.changes.length + 24 | this.minor_changes.length + 25 | this.deprecations.length + 26 | this.revocations.length + 27 | this.unchanged.length 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nav-app/src/app/classes/version.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | public readonly name: string; 3 | public readonly number: string; 4 | 5 | /** 6 | * Creates an instance of Version 7 | * @param name version name 8 | * @param number version number 9 | */ 10 | constructor(name: string, number: string) { 11 | this.name = name; 12 | this.number = number; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nav-app/src/app/help/help.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Documentation 4 |
5 | 6 |
7 |

8 | 9 |
10 |

Table of Contents

11 | 16 |
17 | 18 |
19 | 20 | 21 | 22 |
-------------------------------------------------------------------------------- /nav-app/src/app/help/help.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .help-dialog { 4 | .table-of-contents { 5 | list-style: none; 6 | margin: 0; 7 | padding: 0 25px 0 0; 8 | li.toc-heading { 9 | margin-bottom: 0; 10 | a { 11 | cursor: pointer; 12 | color: blue; 13 | text-decoration: underline; 14 | } 15 | @for $level from 1 through 6 { 16 | &.level-#{$level} { 17 | padding-inline-start: #{40 * ($level)}px; 18 | } 19 | } 20 | } 21 | } 22 | .content { 23 | code { 24 | border: 1px solid color(panel-dark); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nav-app/src/app/help/help.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject, ViewEncapsulation, ViewChild, Renderer2 } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; 3 | import { MarkdownComponent, MarkdownService } from 'ngx-markdown'; 4 | import { LayerInformationComponent } from '../layer-information/layer-information.component'; 5 | 6 | @Component({ 7 | selector: 'help', 8 | templateUrl: './help.component.html', 9 | styleUrls: ['./help.component.scss'], 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class HelpComponent implements OnInit { 13 | private listenObj: any; 14 | @ViewChild('markdownElement', { static: false }) public markdownElement: any; 15 | public headingAnchors: MarkdownHeadingAnchor[] = []; 16 | 17 | constructor( 18 | private dialog: MatDialog, 19 | private markdownService: MarkdownService, 20 | private renderer: Renderer2, 21 | @Inject(MAT_DIALOG_DATA) public data 22 | ) { 23 | // intentionally left blank 24 | } 25 | 26 | ngOnInit(): void { 27 | setTimeout(() => { 28 | this.scrollTo('toc'); 29 | }, 175); 30 | 31 | let self = this; 32 | this.markdownService.renderer.heading = (text: string, level: number) => { 33 | let img = text.match(/()/g) ? text.match(/()/g)[0].replace(/(nav-app\/src\/)/g, '') : ''; 34 | text = text.replace(/()/g, ''); 35 | const escapedText = text 36 | .toLowerCase() 37 | .trim() 38 | .replace(/[^\w]+/g, '-'); 39 | self.headingAnchors.push({ 40 | level: level, 41 | anchor: escapedText, 42 | label: text.replace('&', '&'), 43 | }); 44 | return `${img}${text}`; 45 | }; 46 | 47 | this.markdownService.renderer.html = (html: string) => { 48 | if (!html.match(/(nav-app\/src\/)/g)) return html; 49 | return html.replace(/(nav-app\/src\/)/g, ''); 50 | }; 51 | } 52 | 53 | ngOnDestroy(): void { 54 | if (this.listenObj) { 55 | this.listenObj(); 56 | } 57 | } 58 | 59 | // from https://github.com/jfcere/ngx-markdown/issues/125#issuecomment-518025821 60 | public onMarkdownLoad(e) { 61 | // hijack clicks on links 62 | if (this.markdownElement) { 63 | this.listenObj = this.renderer.listen(this.markdownElement.element.nativeElement, 'click', (e: Event) => { 64 | if (e.target && (e.target as any).tagName === 'A') { 65 | const el = e.target as HTMLElement; 66 | const linkURL = el.getAttribute && el.getAttribute('href'); 67 | if (linkURL) { 68 | e.preventDefault(); 69 | if (linkURL.charAt(0) === '#') this.scrollTo(linkURL.replace('#', '')); 70 | else if (linkURL.includes('layers/')) this.openLayerDialog(); 71 | else if (linkURL.match(/(nav-app\/src\/)/g)) window.open(linkURL.replace(/(nav-app\/src\/)/g, '')); 72 | else window.open(linkURL); 73 | } 74 | } 75 | }); 76 | } 77 | } 78 | 79 | public scrollTo(anchor) { 80 | let element = document.querySelector('.' + anchor); 81 | if (element) element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 82 | } 83 | 84 | /** 85 | * open the layer information dialog 86 | */ 87 | public openLayerDialog(): void { 88 | this.dialog.open(LayerInformationComponent, { 89 | autoFocus: false, 90 | panelClass: this.data.theme, 91 | }); 92 | } 93 | } 94 | 95 | interface MarkdownHeadingAnchor { 96 | level: number; 97 | anchor: string; 98 | label: string; 99 | } 100 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-information/layer-information.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 |
-------------------------------------------------------------------------------- /nav-app/src/app/layer-information/layer-information.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .layer-dialog { 4 | .content { 5 | max-height: 75vh; 6 | font-size: 10pt; 7 | } 8 | 9 | table { 10 | border-collapse: collapse; 11 | width: 100%; 12 | 13 | th, 14 | td { 15 | width: calc(100% / 5); 16 | padding: 5px; 17 | word-break: break-word; 18 | line-height: 16pt; 19 | } 20 | 21 | th:nth-child(4) { 22 | padding: 0 30px; 23 | } 24 | 25 | th:nth-child(3), 26 | th:nth-child(4), 27 | th:last-child, 28 | td:nth-child(3), 29 | td:nth-child(4) { 30 | text-align: center; 31 | } 32 | 33 | td { 34 | vertical-align: middle; 35 | border-top: 1px solid color(panel-dark); 36 | } 37 | 38 | td:not(td:last-child) { 39 | hyphens: auto; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-information/layer-information.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { LayerInformationComponent } from './layer-information.component'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { MarkdownModule, MarkdownService } from 'ngx-markdown'; 5 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 6 | import * as globals from '../utils/globals'; 7 | 8 | describe('LayerInformationComponent', () => { 9 | let component: LayerInformationComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [LayerInformationComponent], 15 | imports: [ 16 | HttpClientModule, 17 | MatDialogModule, 18 | MarkdownModule.forRoot({ loader: HttpClient })], 19 | providers: [MarkdownService] 20 | }).compileComponents(); 21 | 22 | fixture = TestBed.createComponent(LayerInformationComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | 31 | it('should return correct layerFormatLink based on global layer version', () => { 32 | let filePath = `./layers/spec/v${globals.layerVersion}/layerformat.md`; 33 | expect(component.layerFormatLink).toBe(filePath); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-information/layer-information.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import * as globals from '../utils/globals'; 3 | 4 | @Component({ 5 | selector: 'app-layer-information', 6 | templateUrl: './layer-information.component.html', 7 | styleUrls: ['./layer-information.component.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | }) 10 | export class LayerInformationComponent { 11 | public get layerFormatLink(): string { 12 | return `./layers/spec/v${globals.layerVersion}/layerformat.md`; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-settings/layer-settings.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | .layer-settings { 3 | .sidebar-content { 4 | margin: 1rem 0; 5 | } 6 | .info-card { 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | .info-field { 11 | margin: 1em 0; 12 | .mat-mdc-form-field { 13 | width: 100%; 14 | } 15 | } 16 | .settings .title { 17 | font-size: 14px; 18 | margin: 0; 19 | } 20 | .layer-data { 21 | .mat-divider.layer-div { 22 | margin-bottom: 10px; 23 | @include adaptive-color-dark-only('border-top-color', color(dark-4)); 24 | } 25 | } 26 | .sub-section { 27 | padding-top: 1.5em; 28 | } 29 | .padding-top { 30 | padding-top: 1em; 31 | } 32 | .data-input { 33 | overflow-y: auto; 34 | max-height: 30vh; 35 | } 36 | .button-container { 37 | display: flex; 38 | justify-content: flex-end; 39 | } 40 | .mat-mdc-form-field, .mat-mdc-form-field:hover { 41 | &:not(.mat-form-field-disabled) { 42 | .mat-mdc-floating-label, 43 | .mat-mdc-input-element { 44 | @include adaptive-color-dark-only('color', on-color(dark)); 45 | } 46 | .mdc-line-ripple::before { 47 | @include adaptive-color-dark-only('border-bottom-color', on-color(dark-3)); 48 | } 49 | } 50 | 51 | &.mat-form-field-disabled { 52 | .mat-mdc-floating-label, 53 | .mdc-text-field__input { 54 | @include adaptive-color-dark-only('color', darken(on-color(dark-1), 25%)); 55 | } 56 | .mdc-line-ripple::before { 57 | @include adaptive-color-dark-only('border-bottom-color', darken(on-color(dark-1), 25%)); 58 | } 59 | } 60 | .mat-mdc-form-field-hint { 61 | @include adaptive-color-dark-only('color', on-color(dark)); 62 | } 63 | } 64 | .mat-mdc-card { 65 | width: 100%; 66 | } 67 | .mat-mdc-card-title { 68 | padding: 16px 16px 0 16px; 69 | } 70 | .mat-mdc-card + .mat-mdc-card { 71 | margin-left: 1em; 72 | } 73 | } -------------------------------------------------------------------------------- /nav-app/src/app/layer-settings/layer-settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MarkdownModule, MarkdownService } from 'ngx-markdown'; 3 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 4 | import { ViewModel } from '../classes'; 5 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; // Import CUSTOM_ELEMENTS_SCHEMA 6 | import { LayerSettingsComponent } from './layer-settings.component'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { ConfigService } from '../services/config.service'; 9 | import * as MockData from '../../tests/utils/mock-data'; 10 | 11 | describe('LayerSettingsComponent', () => { 12 | let component: LayerSettingsComponent; 13 | let fixture: ComponentFixture; 14 | let configService: ConfigService; 15 | 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [LayerSettingsComponent], 19 | imports: [ 20 | HttpClientModule, 21 | FormsModule, 22 | MarkdownModule.forRoot({ loader: HttpClient }) 23 | ], 24 | providers: [MarkdownService], 25 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 26 | }) 27 | .compileComponents(); 28 | configService = TestBed.inject(ConfigService); 29 | configService.versions = MockData.configData; 30 | fixture = TestBed.createComponent(LayerSettingsComponent); 31 | component = fixture.componentInstance; 32 | component.viewModel = new ViewModel('layer', '35', 'enterprise-attack-13', null); 33 | fixture.detectChanges(); 34 | })); 35 | 36 | it('should create', () => { 37 | expect(component).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-settings/layer-settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { ViewModel } from '../classes'; 3 | import { DataService } from '../services/data.service'; 4 | 5 | @Component({ 6 | selector: 'app-layer-settings', 7 | templateUrl: './layer-settings.component.html', 8 | styleUrls: ['./layer-settings.component.scss'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class LayerSettingsComponent { 12 | @Input() viewModel: ViewModel; 13 | 14 | constructor( 15 | public dataService: DataService, 16 | ) { 17 | // intentionally left blank 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-upgrade/changelog-cell/changelog-cell.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{ tactic.name }} 6 | 7 |
8 |
9 | {{ technique.attackID }} 10 |
11 |
12 | {{ technique.name }} 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-upgrade/changelog-cell/changelog-cell.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../matrix/technique-cell/technique-cell.component.scss'; 2 | 3 | .nopointer { 4 | cursor: unset; 5 | } 6 | .section { 7 | margin: 4px 0; 8 | } 9 | .section + .section { 10 | border-top: 1px solid rgba(border-color(body), 0.5); 11 | } 12 | .technique-cell { 13 | margin: auto; 14 | } 15 | .setwidth { 16 | max-width: 50%; 17 | } 18 | .mat-drawer-inner-container .technique-cell { 19 | @include adaptive-color-dark-only('background-color', color(dark-4)); 20 | } 21 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-upgrade/changelog-cell/changelog-cell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { ViewModelsService } from '../../services/viewmodels.service'; 3 | import { ConfigService } from '../../services/config.service'; 4 | import { DataService } from '../../services/data.service'; 5 | import { Cell } from '../../matrix/cell'; 6 | 7 | @Component({ 8 | selector: 'changelog-cell', 9 | templateUrl: './changelog-cell.component.html', 10 | styleUrls: ['./changelog-cell.component.scss'], 11 | encapsulation: ViewEncapsulation.None, 12 | }) 13 | export class ChangelogCellComponent extends Cell { 14 | @Input() isCurrentVersion?: boolean = true; 15 | @Input() isDraggable?: boolean = false; 16 | @Input() section: string; 17 | 18 | constructor( 19 | public configService: ConfigService, 20 | public dataService: DataService, 21 | public viewModelsService: ViewModelsService 22 | ) { 23 | super(dataService, configService); 24 | } 25 | 26 | /** 27 | * Highlight the moused over technique 28 | */ 29 | public highlight(): void { 30 | if (this.isCurrentVersion) { 31 | this.viewModel.highlightTechnique(this.technique, this.tactic); 32 | } 33 | } 34 | 35 | /** 36 | * Clear the technique highlight 37 | */ 38 | public unhighlight(): void { 39 | if (this.isCurrentVersion) this.viewModel.clearHighlight(); 40 | } 41 | 42 | /** 43 | * Select or unselect this technique 44 | */ 45 | public onClick(): void { 46 | if (this.isCurrentVersion) { 47 | // unselect technique 48 | if (this.viewModel.isTechniqueSelected(this.technique, this.tactic)) { 49 | this.viewModel.unselectTechnique(this.technique, this.tactic); 50 | } 51 | // select technique 52 | else { 53 | this.viewModel.clearSelectedTechniques(); 54 | this.viewModel.selectTechnique(this.technique, this.tactic); 55 | } 56 | this.viewModelsService.selectionChanged(); // emit selection change 57 | } 58 | } 59 | 60 | /** 61 | * Retrieve css classes for this technique 62 | */ 63 | public getClass(): string { 64 | let theclass = super.getClass(); 65 | if (!this.isCurrentVersion && !this.isDraggable) { 66 | theclass += ' nopointer'; 67 | } 68 | if (this.section == 'additions' || this.section == 'deprecations') { 69 | theclass += ' setwidth'; 70 | } 71 | return theclass; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nav-app/src/app/layer-upgrade/layer-upgrade.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .title { 4 | h2 { 5 | display: inline-block; 6 | margin: 12px 0; 7 | } 8 | .mat-icon { 9 | vertical-align: middle; 10 | } 11 | } 12 | h4 { 13 | margin: 12px 0; 14 | } 15 | .mat-divider { 16 | margin: 1rem 0 !important; 17 | } 18 | .changelog-cells { 19 | padding-bottom: 1rem; 20 | position: relative; 21 | } 22 | .clear-annotations { 23 | position: absolute; 24 | top: -10px; 25 | right: -5px; 26 | padding: 6px 6px; 27 | background: color(panel-dark); 28 | border-radius: 10px; 29 | line-height: 10px; 30 | cursor: pointer; 31 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5); 32 | &:hover { 33 | background-color: darken(color(panel-dark), 5%); 34 | } 35 | } 36 | 37 | .mat-step .mat-mdc-button-base { 38 | @include adaptive-color-dark-only('border-color', color(dark-4)); 39 | @include adaptive-color-dark-only('background-color', rgba(0, 0, 0, 0)); 40 | &:disabled { 41 | @include adaptive-color-dark-only('color', rgba(255, 255, 255, 0.5)); 42 | } 43 | } 44 | 45 | .mat-step-header .mat-step-label { 46 | @include adaptive-color-dark-only('color', on-color(dark)); 47 | } 48 | 49 | .mat-stepper-vertical-line::before { 50 | @include adaptive-color-dark-only('border-left-color', on-color(dark)); 51 | } 52 | 53 | .mat-mdc-paginator-container { 54 | min-height: unset; 55 | } 56 | 57 | .mat-stepper-vertical, 58 | .mat-mdc-paginator { 59 | @include adaptive-color-dark-only('background-color', color(dark-3)); 60 | @include adaptive-color-dark-only('color', on-color(dark)); 61 | } 62 | 63 | .stepper-content { 64 | padding: 6px; 65 | } 66 | 67 | .mat-expansion-panel:first-child .mat-expansion-panel-header, 68 | .mat-expansion-panel-header.mat-expanded { 69 | @include adaptive-color-dark-only('border-top', 1px solid #ffffff21); 70 | } 71 | 72 | .mat-expansion-panel-header-title span { 73 | padding-left: 1rem; 74 | display: inline-flex; 75 | align-items: center; 76 | } 77 | 78 | tr td { 79 | padding-bottom: 1rem; 80 | } 81 | .changelog-table { 82 | table-layout: fixed; 83 | width: 100%; 84 | } 85 | .narrow { 86 | width: 44px; 87 | } 88 | .button-container .button { 89 | display: flex; 90 | align-items: center; 91 | margin: 4px; 92 | .mat-icon { 93 | font-size: 16px; 94 | } 95 | &:disabled { 96 | @include adaptive-color('background-color', lighten(color(panel-dark), 5%), color(dark-2)); 97 | cursor: unset; 98 | } 99 | } 100 | .cols { 101 | display: flex; 102 | .float { 103 | float: left; 104 | padding: 6px; 105 | } 106 | } 107 | .version { 108 | text-align: center; 109 | margin-bottom: -1rem; 110 | a { 111 | text-decoration: none; 112 | } 113 | } 114 | .arrow { 115 | text-align: center; 116 | vertical-align: top; 117 | } 118 | .info { 119 | margin-top: 1.5rem; 120 | height: 100%; 121 | display: flex; 122 | .mat-icon { 123 | height: 100%; 124 | } 125 | } 126 | .wide { 127 | width: 100%; 128 | } 129 | .checkbox { 130 | padding: 6px; 131 | &.right { 132 | float: right; 133 | } 134 | } 135 | .checkbox-custom-label { 136 | display: block; 137 | } 138 | .summary { 139 | text-indent: 24px; 140 | } 141 | .stepper-button { 142 | float: right; 143 | padding-bottom: 24px; 144 | .mat-mdc-outlined-button { 145 | margin-left: 12px; 146 | } 147 | } 148 | .reviewed { 149 | color: #4bb543; 150 | } 151 | .disabled { 152 | @include adaptive-color('color', rgba(0, 0, 0, 0.15), color(dark-link)); 153 | } 154 | .dndDragover .technique-cell { 155 | box-shadow: 0 0 0 1px color(cell-highlight-color) inset !important; 156 | } 157 | .mat-vertical-stepper-header { 158 | pointer-events: none; 159 | } 160 | .description { 161 | white-space: pre-line; 162 | padding: 6px; 163 | text-align: justify; 164 | span { 165 | font-size: 14px; 166 | } 167 | &.center { 168 | text-align: center; 169 | } 170 | } 171 | .spinner { 172 | display: flex; 173 | justify-content: center; 174 | align-items: center; 175 | height: 50vh; 176 | 177 | mat-progress-spinner circle { 178 | stroke: on-color-deemphasis(body); 179 | } 180 | } 181 | .mat-icon { 182 | overflow: visible; 183 | } 184 | -------------------------------------------------------------------------------- /nav-app/src/app/list-input/list-input.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 10 |
11 | 12 |
13 | 18 |
19 | 20 | 21 |
22 | 23 | {{config.nameField}} 24 | 29 | 30 | 31 | {{config.valueField}} 32 | 37 | 38 | 43 | {{ item[config.nameField] }} 44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 | -------------------------------------------------------------------------------- /nav-app/src/app/list-input/list-input.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .list-container, 4 | .add-more { 5 | margin: 10px 0; 6 | } 7 | .item-container { 8 | padding: 10px; 9 | border: 1px solid color(panel-dark); 10 | margin: 4px 0; 11 | 12 | &.hide-subscript { 13 | .mat-mdc-form-field + .mat-mdc-form-field { 14 | margin-top: 8px; 15 | } 16 | } 17 | .mat-mdc-form-field { 18 | width: 100%; 19 | } 20 | } 21 | .remove-button { 22 | margin-top: 8px; 23 | display: flex; 24 | justify-content: flex-end; 25 | } 26 | .assigned-link { 27 | float: left; 28 | @include adaptive-color-dark-only('color', color(dark-link) !important); 29 | } 30 | .assigned-link:active, 31 | .assigned-link:visited { 32 | @include adaptive-color-dark-only('color', color(dark-link-active)); 33 | } 34 | 35 | .divider-button-container { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | } 40 | 41 | .divider-button { 42 | display: flex; 43 | width: 100%; 44 | align-items: center; 45 | background: none; 46 | border: none; 47 | cursor: pointer; 48 | padding: 5px 0; 49 | .icon { 50 | font-size: 16px; 51 | height: 16px; 52 | width: 16px; 53 | border-radius: 3px; 54 | padding: 1px; 55 | } 56 | .line { 57 | flex: 1; 58 | height: 2px; 59 | } 60 | &.divider-option { 61 | .line, .icon { 62 | @include adaptive-color('background-color', color(panel-dark), color(dark-4)); 63 | } 64 | } 65 | &.divider { 66 | .line, .icon { 67 | @include adaptive-color('background-color', darken(color(panel-dark), 40%), lighten(color(dark-4), 40%)); 68 | } 69 | } 70 | &:hover .line, &:hover .icon { 71 | @include adaptive-color('background-color', color(button-dark), color(dark-link)); 72 | } 73 | } -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-common.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | $sizeunit: 14px; 4 | $sizeunit_print: 12px; 5 | 6 | .matrix { 7 | border-collapse: collapse; 8 | line-height: $sizeunit; 9 | } 10 | 11 | @media print { 12 | .matrix { 13 | border-collapse: collapse; 14 | line-height: $sizeunit_print; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-flat/matrix-flat.component.scss: -------------------------------------------------------------------------------- 1 | @import '../matrix-common.scss'; 2 | .matrix.flat { 3 | .tactic { 4 | margin-right: 5px; 5 | width: 1%; 6 | vertical-align: top; 7 | 8 | &.name, 9 | &.count { 10 | text-align: center; 11 | font-size: $sizeunit + 2px; 12 | } 13 | &.name { 14 | cursor: pointer; 15 | vertical-align: bottom; 16 | font-weight: bold; 17 | @media print { 18 | font-size: $sizeunit_print - 4px; 19 | } 20 | position: sticky; 21 | top: 0.05px; 22 | z-index: 9999; 23 | @include adaptive-color-dark-only('background-color', lighten(color(dark-1), 3%)); 24 | @include adaptive-color-light-only('background-color', white); 25 | } 26 | 27 | &.count { 28 | @include adaptive-color('border-bottom', 1px solid black, 1px solid white); 29 | font-size: $sizeunit - 1px; 30 | padding-bottom: 5px; 31 | margin-bottom: 5px; 32 | @media print { 33 | font-size: $sizeunit_print - 4px; 34 | } 35 | position: sticky; 36 | top: 32px; 37 | z-index: 9999; 38 | @include adaptive-color-dark-only('background-color', lighten(color(dark-1), 3%)); 39 | @include adaptive-color-light-only('background-color', white); 40 | } 41 | 42 | .subtechniques-row.hidden { 43 | display: none; 44 | } 45 | .supertechnique { 46 | border-collapse: collapse; 47 | width: 100%; 48 | padding: 0; 49 | margin: 0; 50 | td { 51 | padding: 0; 52 | vertical-align: top; 53 | &.sidebar.technique { 54 | min-width: 8px; 55 | width: 12px; 56 | padding: 0; 57 | background: on-color-deemphasis(body); 58 | cursor: pointer; 59 | vertical-align: middle; 60 | .handle { 61 | text-align: center; 62 | vertical-align: middle; 63 | transform: rotate(-90deg); 64 | color: color-alternate(body); 65 | width: 12px; 66 | height: 12px; 67 | font-size: 16px; 68 | line-height: 12px; 69 | } 70 | &.disabled { 71 | background: #aaaaaa; 72 | border-color: #aaaaaa; 73 | box-shadow: none; 74 | pointer-events: none; 75 | } 76 | } 77 | &.sidebar.subtechniques { 78 | svg { 79 | fill: on-color-deemphasis(body); 80 | } 81 | } 82 | &.sidebar { 83 | border-right: 2px solid on-color-deemphasis(body); 84 | @media print { 85 | display: none; 86 | } 87 | } 88 | &.technique { 89 | box-shadow: 0 0 0 1px on-color-deemphasis(body) inset; 90 | } 91 | } 92 | } 93 | 94 | .more-icon { 95 | transition: all ease 0.125s; 96 | vertical-align: top; 97 | transform: scale(0.5) rotate(-90deg); 98 | width: 12px; 99 | height: 12px; 100 | &.expanded { 101 | transform: scale(0.5); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-flat/matrix-flat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { MatrixFlatComponent } from './matrix-flat.component'; 4 | 5 | describe('MatrixFlatComponent', () => { 6 | let component: MatrixFlatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | declarations: [MatrixFlatComponent], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MatrixFlatComponent); 16 | component = fixture.componentInstance; 17 | })); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-flat/matrix-flat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { MatrixCommon } from '../matrix-common'; 3 | import { ConfigService } from '../../services/config.service'; 4 | import { ViewModelsService } from '../../services/viewmodels.service'; 5 | 6 | @Component({ 7 | selector: 'matrix-flat', 8 | templateUrl: './matrix-flat.component.html', 9 | styleUrls: ['./matrix-flat.component.scss'], 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class MatrixFlatComponent extends MatrixCommon implements OnInit { 13 | constructor(configService: ConfigService, viewModelsService: ViewModelsService) { 14 | super(configService, viewModelsService); 15 | } 16 | 17 | ngOnInit(): void { 18 | // intentionally left blank 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-mini/matrix-mini.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 55 |
5 | 6 |
12 | 13 |
14 |
15 | 25 |
26 |
27 | 36 |
37 |
38 | 39 |
40 | 49 |
50 |
51 |
52 |
56 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-mini/matrix-mini.component.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math' as math; 2 | @import '../matrix-common.scss'; 3 | .matrix.mini { 4 | .header { 5 | text-align: left; 6 | } 7 | .tactic { 8 | padding: 0 5px; 9 | } 10 | .tactic.header { 11 | padding: 5px; 12 | } 13 | .tactic.body { 14 | padding-top: 5px; 15 | vertical-align: top; 16 | // display: flex; 17 | // flex-wrap: wrap; 18 | // justify-content: space-between; 19 | 20 | .cell-container { 21 | margin-right: math.div($sizeunit, 6); 22 | margin-bottom: math.div($sizeunit, 6); 23 | &:not(.supertechnique-group) { 24 | display: inline-block; 25 | } 26 | // margin: 0 2px 0 2px; 27 | &.supertechnique-group { 28 | // background: color(panel-light); 29 | // border-top: 1px solid darken(color(panel-light), 5%); 30 | // border-bottom: 1px solid darken(color(panel-light), 5%); 31 | // padding-top: 3px; 32 | // transform: translateX(-3px); 33 | box-shadow: 0 0 0 1px on-color-deemphasis(body) inset; 34 | // padding: 3px 0 0 3px; 35 | padding-top: 1px + math.div($sizeunit, 2); 36 | padding-left: 1px + math.div($sizeunit, 2); 37 | padding-bottom: math.div($sizeunit, 3); 38 | padding-right: math.div($sizeunit, 3); 39 | } 40 | &.supertechnique { 41 | box-shadow: 0 0 0 1px on-color-deemphasis(body); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-mini/matrix-mini.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { MatrixMiniComponent } from './matrix-mini.component'; 4 | 5 | describe('MatrixMiniComponent', () => { 6 | let component: MatrixMiniComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | declarations: [MatrixMiniComponent], 13 | }).compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MatrixMiniComponent); 18 | component = fixture.componentInstance; 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-mini/matrix-mini.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatrixCommon } from '../matrix-common'; 3 | import { ConfigService } from '../../services/config.service'; 4 | import { ViewModelsService } from '../../services/viewmodels.service'; 5 | 6 | @Component({ 7 | selector: 'matrix-mini', 8 | templateUrl: './matrix-mini.component.html', 9 | styleUrls: ['./matrix-mini.component.scss'], 10 | }) 11 | export class MatrixMiniComponent extends MatrixCommon implements OnInit { 12 | constructor(configService: ConfigService, viewModelsService: ViewModelsService) { 13 | super(configService, viewModelsService); 14 | } 15 | 16 | ngOnInit(): void { 17 | // intentionally left blank 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-side/matrix-side.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { MatrixSideComponent } from './matrix-side.component'; 4 | import { MatDialogModule } from '@angular/material/dialog'; 5 | 6 | describe('MatrixSideComponent', () => { 7 | let component: MatrixSideComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [HttpClientTestingModule, MatDialogModule], 13 | declarations: [MatrixSideComponent], 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(MatrixSideComponent); 19 | component = fixture.componentInstance; 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/matrix-side/matrix-side.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { MatrixCommon } from '../matrix-common'; 3 | import { ConfigService } from '../../services/config.service'; 4 | import { ViewModelsService } from '../../services/viewmodels.service'; 5 | 6 | @Component({ 7 | selector: 'matrix-side', 8 | templateUrl: './matrix-side.component.html', 9 | styleUrls: ['./matrix-side.component.scss'], 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class MatrixSideComponent extends MatrixCommon implements OnInit { 13 | constructor(configService: ConfigService, viewModelsService: ViewModelsService) { 14 | super(configService, viewModelsService); 15 | } 16 | 17 | ngOnInit(): void { 18 | // intentionally left blank 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/tactic-cell/tactic-cell.component.html: -------------------------------------------------------------------------------- 1 |
8 | {{ tactic.attackID }} 9 |
10 | {{ tactic.name }} 11 |
12 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/tactic-cell/tactic-cell.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../colors.scss'; 2 | @import '../matrix-common.scss'; 3 | .tactic-cell { 4 | padding-top: 3px; 5 | box-sizing: border-box; 6 | &:not(.mini) { 7 | min-width: $sizeunit * 3; 8 | } 9 | &.mini { 10 | @include adaptive-color('background', black, white); 11 | width: $sizeunit; 12 | } 13 | min-height: $sizeunit; 14 | &.bordered { 15 | @include adaptive-color('border', 1px solid border-color(body), 1px solid white); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/tactic-cell/tactic-cell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { TechniqueVM, ViewModel } from '../../classes'; 3 | import { Tactic, Technique } from '../../classes/stix'; 4 | import { TacticCellComponent } from './tactic-cell.component'; 5 | import * as MockData from '../../../tests/utils/mock-data'; 6 | import { MatTooltipModule } from '@angular/material/tooltip'; 7 | 8 | describe('TacticCellComponent', () => { 9 | let component: TacticCellComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [MatTooltipModule], 15 | declarations: [TacticCellComponent], 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | let technique_list: Technique[] = []; 21 | fixture = TestBed.createComponent(TacticCellComponent); 22 | component = fixture.componentInstance; 23 | component.viewModel = new ViewModel('layer', '33', 'enterprise-attack-13', null); 24 | component.viewModel.domainVersionID = 'enterprise-attack-13'; 25 | let t1 = new Technique(MockData.T0000, [], null); 26 | technique_list.push(t1); 27 | let tvm_1 = new TechniqueVM('T1595^reconnaissance'); 28 | component.viewModel.setTechniqueVM(tvm_1); 29 | component.tactic = new Tactic(MockData.TA0000, technique_list, null); 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should create', () => { 34 | expect(component).toBeTruthy(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/tactic-cell/tactic-cell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewEncapsulation } from '@angular/core'; 2 | import { Tactic } from '../../classes/stix'; 3 | import { ViewModel } from '../../classes'; 4 | 5 | @Component({ 6 | selector: 'tactic-cell', 7 | templateUrl: './tactic-cell.component.html', 8 | styleUrls: ['./tactic-cell.component.scss'], 9 | encapsulation: ViewEncapsulation.None, 10 | }) 11 | export class TacticCellComponent implements OnInit { 12 | @Input() tactic: Tactic; 13 | @Input() viewModel: ViewModel; 14 | 15 | constructor() { 16 | // intentionally left blank 17 | } 18 | 19 | ngOnInit() { 20 | // intentionally left blank 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/cell-popover.scss: -------------------------------------------------------------------------------- 1 | @mixin cell-popover($opacity) { 2 | position: absolute; 3 | padding: 6px; 4 | &:not(.top) { 5 | top: 0; 6 | } 7 | z-index: 100; 8 | min-width: 150px; 9 | max-width: 300px; 10 | text-align: left; 11 | white-space: normal; 12 | 13 | border-radius: 3px; 14 | font-size: 8pt; 15 | color: white; 16 | &.bottom.right { 17 | /* ------------------ 18 | * | | 19 | * | X XXXX | 20 | * | XXXX | 21 | * ------------------ 22 | */ 23 | transform: translateX(10px); 24 | &:before { 25 | left: -9.5px; 26 | transform: rotate(90deg); 27 | } 28 | } 29 | &.bottom.left { 30 | /* ------------------ 31 | * | | 32 | * | XXXX X | 33 | * | XXXX | 34 | * ------------------ 35 | */ 36 | transform: translateX(-10px); 37 | right: 100%; 38 | &:before { 39 | right: -9px; 40 | transform: rotate(-90deg); 41 | } 42 | } 43 | &.top.right { 44 | /* ------------------ 45 | * | XXXX | 46 | * | XXXX | 47 | * | X | 48 | * | | 49 | * ------------------ 50 | */ 51 | transform: translateY(-10px); 52 | bottom: 100%; 53 | left: 0; 54 | &:before { 55 | bottom: -9px; 56 | // transform: rotate(180deg); 57 | } 58 | } 59 | &.top.left { 60 | /* ------------------ 61 | * | XXXX | 62 | * | XXXX | 63 | * | X | 64 | * | | 65 | * ------------------ 66 | */ 67 | transform: translateY(-10px); 68 | bottom: 100%; 69 | right: 0; 70 | &:before { 71 | bottom: -9px; 72 | right: 6px; 73 | // transform: rotate(180deg); 74 | } 75 | } 76 | &:before { 77 | position: absolute; 78 | font-size: 12px; 79 | content: '\25BC'; 80 | color: rgba(80, 80, 80, $opacity); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/cell-popover.ts: -------------------------------------------------------------------------------- 1 | import { ElementRef } from '@angular/core'; 2 | 3 | export abstract class CellPopover { 4 | private theElement: any; 5 | constructor(element: ElementRef) { 6 | this.theElement = element.nativeElement; 7 | } 8 | 9 | /** 10 | * Get the location of the tooltip according to the location on the screen. 11 | * Returns one of the following: 12 | * "top left": 13 | * ------------------ 14 | * | XXXX | 15 | * | XXXX | 16 | * | X | 17 | * | | 18 | * ------------------ 19 | * "top right": 20 | * ------------------ 21 | * | XXXX | 22 | * | XXXX | 23 | * | X | 24 | * | | 25 | * ------------------ 26 | * "bottom left": 27 | * ------------------ 28 | * | | 29 | * | XXXX X | 30 | * | XXXX | 31 | * ------------------ 32 | * "bottom right": 33 | * ------------------ 34 | * | | 35 | * | X XXXX | 36 | * | XXXX | 37 | * ------------------ 38 | * @returns {string} direction 39 | */ 40 | public getPosition(): string { 41 | let boundingRect = this.theElement.getBoundingClientRect(); 42 | let halfWidth = window.innerWidth / 2; 43 | let halfHeight = window.innerHeight / 2; 44 | let position = []; 45 | if (boundingRect.right > halfWidth) position.push('left'); 46 | else position.push('right'); 47 | if (boundingRect.bottom > halfHeight) position.push('top'); 48 | else position.push('bottom'); 49 | return position.join(' '); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/contextmenu/contextmenu.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
{{ technique.name }} ({{ technique.attackID }})
8 |
9 |
10 |
pin/unpin tooltip
11 |
12 |
13 |
select
14 |
add to selection
15 |
remove from selection
16 |
17 |
18 |
select all
19 |
deselect all
20 |
invert selection
21 |
22 |
23 |
select annotated
24 |
select unannotated
25 |
26 |
27 |
select all techniques in tactic
28 |
deselect all techniques in tactic
29 |
30 |
31 |
view technique
32 |
view tactic
33 |
34 |
35 |
39 | {{ contextMenuItem.label }} 40 |
41 |
42 |
43 | 51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/contextmenu/contextmenu.component.scss: -------------------------------------------------------------------------------- 1 | @import '../cell-popover.scss'; 2 | @import '../../../../colors.scss'; 3 | .contextmenu { 4 | .cover { 5 | z-index: 100; 6 | // background: red; 7 | // opacity: 0.5; 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | width: 100vw; 12 | height: 100vh; 13 | cursor: default; 14 | } 15 | .menu { 16 | cursor: default; 17 | @include cell-popover(0.85); 18 | @include adaptive-color('background', rgba(80, 80, 80, 0.85), rgba(color(cell-highlight-dark-color), 0.85)); 19 | .contextMenu-section { 20 | &:not(:first-child) { 21 | border-top: 1px solid white; 22 | margin-top: 2px; 23 | padding-top: 2px; 24 | } 25 | div:not(.link-container) { 26 | padding: 3px; 27 | } 28 | .divider { 29 | border-top: 1px solid white; 30 | margin-top: 2px; 31 | padding: 2px 0 0 0 !important; 32 | } 33 | .contextMenu-button { 34 | cursor: pointer; 35 | &:hover { 36 | @include adaptive-color('background', color(cell-highlight-color), darken(color(cell-highlight-dark-color), 10%)); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/contextmenu/contextmenu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter, ElementRef, ViewEncapsulation } from '@angular/core'; 2 | import { ContextMenuItem, Link, TechniqueVM, ViewModel } from '../../../classes'; 3 | import { Technique, Tactic } from '../../../classes/stix'; 4 | import { ViewModelsService } from '../../../services/viewmodels.service'; 5 | import { ConfigService } from '../../../services/config.service'; 6 | import { CellPopover } from '../cell-popover'; 7 | 8 | @Component({ 9 | selector: 'app-contextmenu', 10 | templateUrl: './contextmenu.component.html', 11 | styleUrls: ['./contextmenu.component.scss'], 12 | encapsulation: ViewEncapsulation.None, 13 | }) 14 | export class ContextmenuComponent extends CellPopover implements OnInit { 15 | @Input() technique: Technique; 16 | @Input() tactic: Tactic; 17 | @Input() viewModel: ViewModel; 18 | public placement: string; 19 | @Output() close = new EventEmitter(); 20 | 21 | public get techniqueVM(): TechniqueVM { 22 | return this.viewModel.getTechniqueVM(this.technique, this.tactic); 23 | } 24 | 25 | public get links(): Link[] { 26 | return this.techniqueVM.links; 27 | } 28 | 29 | constructor( 30 | private element: ElementRef, 31 | public configService: ConfigService, 32 | public viewModelsService: ViewModelsService 33 | ) { 34 | super(element); 35 | } 36 | 37 | ngOnInit() { 38 | this.placement = this.getPosition(); 39 | } 40 | 41 | public closeContextmenu() { 42 | this.close.emit(); 43 | } 44 | 45 | public select() { 46 | this.viewModel.clearSelectedTechniques(); 47 | this.viewModel.selectTechnique(this.technique, this.tactic); 48 | this.closeContextmenu(); 49 | } 50 | 51 | public addSelection() { 52 | this.viewModel.selectTechnique(this.technique, this.tactic); 53 | this.closeContextmenu(); 54 | } 55 | 56 | public removeSelection() { 57 | this.viewModel.unselectTechnique(this.technique, this.tactic); 58 | this.closeContextmenu(); 59 | } 60 | 61 | public selectAll() { 62 | this.viewModel.selectAllTechniques(); 63 | this.closeContextmenu(); 64 | } 65 | 66 | public deselectAll() { 67 | this.viewModel.clearSelectedTechniques(); 68 | this.closeContextmenu(); 69 | } 70 | 71 | public invertSelection() { 72 | this.viewModel.invertSelection(); 73 | this.closeContextmenu(); 74 | } 75 | 76 | public selectAnnotated() { 77 | this.viewModel.selectAnnotated(); 78 | this.closeContextmenu(); 79 | } 80 | 81 | public selectUnannotated() { 82 | this.viewModel.selectUnannotated(); 83 | this.closeContextmenu(); 84 | } 85 | 86 | public selectAllInTactic() { 87 | this.viewModel.selectAllTechniquesInTactic(this.tactic); 88 | this.closeContextmenu(); 89 | } 90 | 91 | public deselectAllInTactic() { 92 | this.viewModel.unselectAllTechniquesInTactic(this.tactic); 93 | this.closeContextmenu(); 94 | } 95 | 96 | public viewTechnique() { 97 | window.open(this.technique.url, '_blank'); 98 | this.closeContextmenu(); 99 | } 100 | 101 | public viewTactic() { 102 | window.open(this.tactic.url, '_blank'); 103 | this.closeContextmenu(); 104 | } 105 | 106 | public pinCell() { 107 | this.viewModelsService.pinnedCell = 108 | this.viewModelsService.pinnedCell === this.techniqueVM.technique_tactic_union_id ? '' : this.techniqueVM.technique_tactic_union_id; 109 | this.closeContextmenu(); 110 | } 111 | 112 | public openCustomContextMenuItem(customItem: ContextMenuItem) { 113 | window.open(customItem.getReplacedURL(this.technique, this.tactic), '_blank'); 114 | this.closeContextmenu(); 115 | } 116 | 117 | public openLink(link: Link) { 118 | window.open(link.url); 119 | this.closeContextmenu(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/technique-cell.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{ technique.attackID }} 5 |
6 | {{ technique.name }} 7 |
8 | 9 | 10 |  ({{ annotatedSubtechniques() }}/{{ applyControls(technique.subtechniques, tactic).length }}) 11 | 12 | 13 |
14 | 15 | 21 |
22 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/technique-cell.component.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math' as math; 2 | @import '../matrix-common.scss'; 3 | .technique-cell { 4 | @include adaptive-color('background-color', white, color(dark-2)); 5 | @include adaptive-color('color', black, on-color(dark-3)); 6 | cursor: pointer; 7 | height: 100%; 8 | display: flex; 9 | align-items: center; 10 | 11 | position: relative; 12 | 13 | font-size: $sizeunit - 1px; 14 | line-height: $sizeunit; 15 | 16 | min-width: $sizeunit; 17 | min-height: $sizeunit; 18 | @media print { 19 | font-size: $sizeunit_print - 5px; 20 | } 21 | 22 | > div { 23 | padding: math.div($sizeunit, 2) 3px; 24 | box-sizing: border-box; 25 | display: block; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | &.showID:not(.showName) { 31 | text-align: center; 32 | } 33 | 34 | .id-name-break { 35 | outline-offset: -0.5px; 36 | outline: 1px solid rgba(border-color(body), 0.5); 37 | } 38 | 39 | &:not(.editing):not(.colored):not(.supertechniquecell) { 40 | @include adaptive-color('box-shadow', 0 0 0 1px border-color(body) inset, 0 0 0 1px border-color(dark-2) inset); 41 | } 42 | 43 | &.editing { 44 | @include adaptive-color('box-shadow', 0 0 0 1px black inset, 0 0 0 1px color(cell-highlight-dark-color) inset); 45 | } 46 | 47 | &.underlined { 48 | &.mini div:before { 49 | position: absolute; 50 | content: 'i'; 51 | font-size: $sizeunit - 1px; 52 | text-align: center; 53 | width: 100%; 54 | top: 0; 55 | left: 0; 56 | } 57 | div span { 58 | border-bottom: 2px solid transparent; 59 | } 60 | } 61 | 62 | &.mini.disabled div:before { 63 | position: absolute; 64 | content: '\2715'; 65 | font-size: $sizeunit -1px; 66 | text-align: center; 67 | width: 100%; 68 | top: 0; 69 | left: 0; 70 | } 71 | 72 | &.highlight { 73 | @include adaptive-color('background', color(cell-highlight-color), color(cell-highlight-dark-color)); 74 | .id-name-break { 75 | outline: 1px solid rgba(black, 0.5); 76 | } 77 | } 78 | 79 | &.unannotated { 80 | .sub { 81 | color: gray; 82 | } 83 | } 84 | } 85 | 86 | .sub { 87 | position: relative; 88 | font-size: 75%; 89 | line-height: 0; 90 | vertical-align: baseline; 91 | bottom: -0.5em; 92 | @media print { 93 | display: none; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/technique-cell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, Output, ViewEncapsulation } from '@angular/core'; 2 | import { DataService } from '../../services/data.service'; 3 | import { Technique, Tactic, Matrix } from '../../classes/stix'; 4 | import { ConfigService } from '../../services/config.service'; 5 | import { Cell } from '../cell'; 6 | import { ViewModelsService } from '../../services/viewmodels.service'; 7 | 8 | @Component({ 9 | selector: 'technique-cell', 10 | templateUrl: './technique-cell.component.html', 11 | styleUrls: ['./technique-cell.component.scss'], 12 | encapsulation: ViewEncapsulation.None, 13 | }) 14 | export class TechniqueCellComponent extends Cell implements OnInit { 15 | @Input() matrix: Matrix; 16 | @Output() highlight = new EventEmitter(); // emit with the highlighted technique, or null to unhighlight 17 | @Output() unhighlight = new EventEmitter(); 18 | @Output() leftclick = new EventEmitter(); // emit with the selected technique and the modifier keys 19 | 20 | public get isCellPinned(): boolean { 21 | return this.viewModelsService.pinnedCell === this.viewModel.getTechniqueVM(this.technique, this.tactic).technique_tactic_union_id; 22 | } 23 | 24 | public get showTooltip(): boolean { 25 | if (this.isCellPinned) return true; 26 | if (this.showContextmenu) return false; 27 | if (this.viewModel.highlightedTechniques.size === 0) return false; 28 | 29 | return ( 30 | this.viewModel.highlightedTechnique === this.technique && 31 | this.viewModel.highlightedTactic && 32 | this.viewModel.highlightedTactic.id === this.tactic.id 33 | ); 34 | } 35 | 36 | constructor( 37 | public dataService: DataService, 38 | public configService: ConfigService, 39 | public viewModelsService: ViewModelsService 40 | ) { 41 | super(dataService, configService); 42 | } 43 | 44 | ngOnInit(): void { 45 | // intentionally left blank 46 | } 47 | 48 | // count number of annotated sub-techniques on this technique 49 | public annotatedSubtechniques(): number { 50 | let annotatedSubs: Technique[] = []; 51 | for (let s of this.technique.subtechniques) { 52 | let subVM = this.viewModel.getTechniqueVM(s, this.tactic); 53 | if (subVM.annotated()) annotatedSubs.push(s); 54 | } 55 | return this.applyControls(annotatedSubs, this.tactic).length; 56 | } 57 | 58 | // sort and filter techniques 59 | public applyControls(techniques: Technique[], tactic: Tactic): Technique[] { 60 | return this.viewModel.applyControls(techniques, tactic, this.matrix); 61 | } 62 | 63 | // events to pass to parent component 64 | public onMouseEnter() { 65 | this.highlight.emit(); 66 | } 67 | public onMouseLeave() { 68 | this.unhighlight.emit(); 69 | } 70 | public onLeftClick(event) { 71 | if (!this.isCellPinned) this.viewModelsService.pinnedCell = ''; 72 | if (this.configService.getFeature('selecting_techniques')) 73 | this.leftclick.emit({ 74 | technique: this.technique, 75 | // modifier keys 76 | shift: event.shiftKey, 77 | ctrl: event.ctrlKey, 78 | meta: event.metaKey, 79 | // position of event on page 80 | x: event.pageX, 81 | y: event.pageY, 82 | }); 83 | else this.onRightClick(event); 84 | } 85 | public onRightClick(event) { 86 | if (!this.isCellPinned) this.viewModelsService.pinnedCell = ''; 87 | this.showContextmenu = true; 88 | } 89 | 90 | // return css classes for a technique 91 | public getClass(): string { 92 | let theclass = super.getClass(); 93 | 94 | // classes by annotated sub-techniques 95 | if (!this.annotatedSubtechniques()) theclass += ' unannotated'; 96 | 97 | if (this.isCellPinned) theclass += ' editing'; 98 | 99 | return theclass; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
5 | {{ technique.name }} ({{ technique.attackID }}) 6 | 7 | push_pin 8 | 9 |
Disabled
Score:{{ techniqueVM.score }}
Aggregate Score ({{ viewModel.layout.aggregateFunction }}):{{ techniqueVM.aggregateScore }}
Comment:{{ techniqueVM.comment }}
{{ note.abstract }}:{{ note.content }}

{{ metadata.name }}:{{ metadata.value }}
36 |
37 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.scss: -------------------------------------------------------------------------------- 1 | @import '../cell-popover.scss'; 2 | @import '../../../../colors'; 3 | 4 | .tooltip { 5 | @include cell-popover(0.75); 6 | @include adaptive-color('background', rgba(80, 80, 80, 0.75), rgba(#464dff, 0.75)); 7 | max-height: 45vh; 8 | overflow: scroll; 9 | padding-bottom: 1em; 10 | z-index: 1000; 11 | cursor: default; 12 | table { 13 | width: 100%; 14 | border-collapse: collapse; 15 | td { 16 | padding: 3px; 17 | vertical-align: top; 18 | white-space: pre-line !important; 19 | } 20 | td:first-child { 21 | white-space: nowrap; 22 | padding-bottom: 3px !important; 23 | } 24 | td.wrap { 25 | white-space: normal; 26 | } 27 | tr:nth-child(2) td { 28 | border-top: 1px solid white; 29 | } 30 | tr .divider { 31 | margin: 0; 32 | padding: 0; 33 | hr { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | } 38 | .technique-name span { 39 | vertical-align: middle; 40 | } 41 | .pin { 42 | @include adaptive-color('background-color', rgba(160, 160, 160, 0.75), rgba(#8f92fe, 0.75)); 43 | border-radius: 4px; 44 | float: right; 45 | cursor: pointer; 46 | 47 | .mat-icon { 48 | font-size: 16px; 49 | height: 20px; 50 | width: 20px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ElementRef, ViewEncapsulation } from '@angular/core'; 2 | import { DataService } from '../../../services/data.service'; 3 | import { Technique, Tactic, Note } from '../../../classes/stix'; 4 | import { ViewModel, TechniqueVM } from '../../../classes'; 5 | import { ViewModelsService } from '../../../services/viewmodels.service'; 6 | import { CellPopover } from '../cell-popover'; 7 | 8 | @Component({ 9 | selector: 'app-tooltip', 10 | templateUrl: './tooltip.component.html', 11 | styleUrls: ['./tooltip.component.scss'], 12 | encapsulation: ViewEncapsulation.None, 13 | }) 14 | export class TooltipComponent extends CellPopover implements OnInit { 15 | @Input() technique: Technique; 16 | @Input() tactic: Tactic; 17 | @Input() viewModel: ViewModel; 18 | public placement: string; 19 | public notes: Note[]; 20 | 21 | public get isCellPinned(): boolean { 22 | return this.viewModelsService.pinnedCell === this.techniqueVM.technique_tactic_union_id; 23 | } 24 | 25 | public get techniqueVM(): TechniqueVM { 26 | return this.viewModel.getTechniqueVM(this.technique, this.tactic); 27 | } 28 | 29 | constructor( 30 | public element: ElementRef, 31 | public dataService: DataService, 32 | public viewModelsService: ViewModelsService 33 | ) { 34 | super(element); 35 | } 36 | 37 | ngOnInit() { 38 | this.placement = this.getPlacement(); 39 | let domain = this.dataService.getDomain(this.viewModel.domainVersionID); 40 | this.notes = domain.notes.filter((note) => { 41 | return note.object_refs.includes(this.technique.id); 42 | }); 43 | } 44 | 45 | public getPlacement(): string { 46 | return this.getPosition(); 47 | } 48 | 49 | public unpin(): void { 50 | this.viewModelsService.pinnedCell = ''; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /nav-app/src/app/search-and-multiselect/search-and-multiselect.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .search-and-multiselect { 4 | mat-label { 5 | padding-left: 5px; 6 | } 7 | 8 | .mat-mdc-form-field { 9 | @include adaptive-color-dark-only('color', on-color(dark)); 10 | } 11 | 12 | .mat-mdc-input-element, 13 | .mat-mdc-input-element::placeholder { 14 | @include adaptive-color-dark-only('color', on-color(dark)); 15 | } 16 | 17 | .mat-form-field-appearance-outline { 18 | width: 100%; 19 | } 20 | 21 | .sidebar-content { 22 | margin: 1rem 0; 23 | } 24 | 25 | .mat-content { 26 | display: block; 27 | } 28 | 29 | .settings .title { 30 | font-size: 14px; 31 | margin: 0 0 0 5px; 32 | } 33 | 34 | .fields { 35 | text-align: left; 36 | margin: 8px 0; 37 | .field { 38 | display: inline-block; 39 | } 40 | } 41 | 42 | h1 { 43 | font-size: 14px; 44 | background: color(panel-light); 45 | padding: 5px; 46 | margin: 0; 47 | text-align: center; 48 | } 49 | 50 | .allresults-buttons { 51 | display: flex; 52 | justify-content: space-around; 53 | 54 | button { 55 | width: 48.5%; 56 | } 57 | } 58 | 59 | button { 60 | @include adaptive-color-dark-only('background-color', color(dark-4)); 61 | @include adaptive-color-dark-only('color', on-color(dark)); 62 | } 63 | 64 | button:hover { 65 | @include adaptive-color('background', color(cell-highlight-color), color(dark-link)); 66 | } 67 | 68 | .results { 69 | max-height: 150px; 70 | overflow-y: auto; 71 | 72 | table { 73 | border-collapse: collapse; 74 | width: 100%; 75 | 76 | tr + tr { 77 | @include adaptive-color('border-top', 1px solid color(panel-light), 1px solid color(dark-4)); 78 | } 79 | 80 | tr:hover { 81 | @include adaptive-color('background', color(cell-highlight-color), color(cell-highlight-dark-color)); 82 | } 83 | 84 | td:first-of-type { 85 | width: 200px; 86 | } 87 | 88 | td { 89 | text-align: left; 90 | 91 | & + td { 92 | width: 1px; 93 | } 94 | } 95 | } 96 | 97 | .no-results { 98 | padding: 5px; 99 | text-align: center; 100 | } 101 | } 102 | 103 | mat-panel-description { 104 | align-items: center; 105 | } 106 | 107 | .button-container { 108 | display: flex; 109 | justify-content: flex-end; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /nav-app/src/app/services/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { ConfigService } from './config.service'; 4 | import * as MockData from '../../tests/utils/mock-data'; 5 | 6 | describe('ConfigService', () => { 7 | let service: ConfigService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | providers: [ConfigService], 13 | }); 14 | service = TestBed.inject(ConfigService); 15 | }); 16 | 17 | it('should be created', () => { 18 | expect(service).toBeTruthy(); 19 | }); 20 | 21 | it('should set feature object', () => { 22 | expect(service.setFeature_object(MockData.configTechniqueControls)).toEqual(['disable_techniques', 'manual_color', 'background_color']); 23 | }); 24 | 25 | it('should set single feature to given value', () => { 26 | expect(service.setFeature('sticky_toolbar', true)).toEqual(['sticky_toolbar']); 27 | }); 28 | 29 | it('should get feature', () => { 30 | service.setFeature('sticky_toolbar', true); 31 | expect(service.getFeature('sticky_toolbar')).toBeTruthy(); 32 | }); 33 | 34 | it('should get feature group', () => { 35 | expect(service.getFeatureGroup('technique_controls')).toBeTruthy(); 36 | service.setFeature_object(MockData.configTechniqueControls); 37 | expect(service.getFeatureGroup('technique_controls')).toBeTruthy(); 38 | }); 39 | 40 | it('should get feature group count', () => { 41 | expect(service.getFeatureGroupCount('technique_controls')).toEqual(-1); 42 | service.setFeature_object(MockData.configTechniqueControls); 43 | expect(service.getFeatureGroupCount('technique_controls')).toEqual(3); 44 | }); 45 | 46 | it('should check if feature exists', () => { 47 | service.setFeature_object(MockData.configTechniqueControls); 48 | expect(service.isFeature('disable_techniques')).toBeTruthy(); 49 | }); 50 | 51 | it('should check if feature group exists', () => { 52 | service.setFeature_object(MockData.configTechniqueControls); 53 | expect(service.isFeatureGroup('technique_controls')).toBeTruthy(); 54 | }); 55 | 56 | it('should set features of the given group to provided value', () => { 57 | service.setFeature_object(MockData.configTechniqueControls); 58 | expect(service.setFeature('technique_controls', true)).toEqual(['technique_controls']); 59 | }); 60 | 61 | it('should set features of the given group to the value object', () => { 62 | let value_object = { scoring: true, comments: false }; 63 | expect(service.setFeature('technique_controls', value_object)).toEqual(['scoring', 'comments']); 64 | }); 65 | 66 | it('should get all url fragments', () => { 67 | let fragments = new Map(); 68 | expect(service.getAllFragments()).toEqual(fragments); 69 | fragments.set('comments', 'false'); 70 | expect(service.getAllFragments('https://mitre-attack.github.io/attack-navigator/#comments=false')).toEqual(fragments); 71 | }); 72 | 73 | it('should set up data in constructor', () => { 74 | expect(service).toBeTruthy(); 75 | }); 76 | 77 | it('should pass validation if only versions is configured', () => { 78 | expect(() => service.validateConfig(MockData.versionsConfig)).not.toThrow(); 79 | }); 80 | 81 | it('should pass validation if only collection index is configured', () => { 82 | expect(() => service.validateConfig(MockData.collectionIndexConfig)).not.toThrow(); 83 | }); 84 | 85 | it('should fail validation if collection_index_url is not a string', () => { 86 | expect(() => service.validateConfig(MockData.invalidTypeConfig)).toThrowError(); 87 | }); 88 | 89 | it('should fail validation if neither versions or collection index are configured', () => { 90 | expect(() => service.validateConfig(MockData.invalidConfig)).toThrowError(); 91 | }); 92 | 93 | it('should return config if validation passes', () => { 94 | expect(() => service.validateConfig(MockData.customConfig)).not.toThrow(); 95 | expect(service.validateConfig(MockData.customConfig)).toEqual(MockData.customConfig); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /nav-app/src/app/services/icons.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { Icons, IconsService } from './icons.service'; 4 | 5 | describe('IconsService', () => { 6 | let service: IconsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(IconsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | 17 | it('should register', () => { 18 | spyOn(service.matIconRegistry, 'addSvgIcon'); 19 | service.registerIcons(); 20 | expect(service.matIconRegistry.addSvgIcon).toHaveBeenCalledTimes(Object.values(Icons).length); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /nav-app/src/app/services/icons.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class IconsService { 9 | constructor( 10 | public matIconRegistry: MatIconRegistry, 11 | private domSanitizer: DomSanitizer 12 | ) { 13 | // intentionally left blank 14 | } 15 | 16 | public registerIcons(): void { 17 | this.loadIcons(Object.values(Icons), 'assets/icons'); 18 | } 19 | 20 | private loadIcons(iconKeys: string[], iconUrl: string): void { 21 | iconKeys.forEach((key) => { 22 | this.matIconRegistry.addSvgIcon(key, this.domSanitizer.bypassSecurityTrustResourceUrl(`${iconUrl}/${key}.svg`)); 23 | }); 24 | } 25 | } 26 | 27 | export enum Icons { 28 | SORT_ALPHABETICAL_ASC = 'ic_sort_alphabetically_ascending', 29 | SORT_ALPHABETICAL_DESC = 'ic_sort_alphabetically_descending', 30 | SORT_NUMERICAL_ASC = 'ic_sort_numerically_ascending', 31 | SORT_NUMERICAL_DESC = 'ic_sort_numerically_descending', 32 | UNFOLD_MORE_ALT = 'ic_unfold_more_alt', 33 | NON_STICKY_TOOLBAR = 'ic_push_pin_gray', 34 | } 35 | -------------------------------------------------------------------------------- /nav-app/src/app/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /nav-app/src/app/sidebar/sidebar.component.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 38em; 3 | padding: 16px; 4 | } -------------------------------------------------------------------------------- /nav-app/src/app/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { SidebarComponent } from './sidebar.component'; 4 | 5 | describe('SidebarComponent', () => { 6 | let component: SidebarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | declarations: [SidebarComponent], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SidebarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | 26 | it('should set reloadToggle to false', () => { 27 | component.ngOnChanges(); 28 | expect(component.reloadToggle).toEqual(false); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /nav-app/src/app/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges } from '@angular/core'; 2 | import { DataService } from '../services/data.service'; 3 | import { ViewModel } from '../classes'; 4 | 5 | @Component({ 6 | selector: 'sidebar', 7 | templateUrl: './sidebar.component.html', 8 | styleUrls: ['./sidebar.component.scss'], 9 | }) 10 | export class SidebarComponent implements OnChanges { 11 | @Input() viewModel: ViewModel; 12 | public reloadToggle: boolean = true; 13 | 14 | constructor(public dataService: DataService) {} 15 | 16 | ngOnChanges(): void { 17 | this.reloadToggle = false; 18 | setTimeout(() => (this.reloadToggle = true)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nav-app/src/app/svg-export/renderable-objects/index.ts: -------------------------------------------------------------------------------- 1 | export { RenderableMatrix } from './renderable-matrix'; 2 | export { RenderableTactic } from './renderable-tactic'; 3 | export { RenderableTechnique } from './renderable-technique'; 4 | -------------------------------------------------------------------------------- /nav-app/src/app/svg-export/renderable-objects/renderable-matrix.ts: -------------------------------------------------------------------------------- 1 | import { Matrix } from '../../classes/stix'; 2 | import { RenderableTactic } from './renderable-tactic'; 3 | import { ViewModel } from '../../classes'; 4 | 5 | export class RenderableMatrix { 6 | public matrix: Matrix; 7 | public tactics: RenderableTactic[] = []; 8 | 9 | public get height() { 10 | let heights = this.tactics.map(function (tactic: RenderableTactic) { 11 | return tactic.height; 12 | }); 13 | return Math.max(...heights); 14 | } 15 | 16 | constructor(matrix: Matrix, viewModel: ViewModel, renderConfig: any) { 17 | this.matrix = matrix; 18 | let filteredTactics = viewModel.filterTactics(matrix.tactics, matrix); 19 | for (let tactic of filteredTactics) { 20 | this.tactics.push(new RenderableTactic(tactic, matrix, viewModel, renderConfig)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nav-app/src/app/svg-export/renderable-objects/renderable-tactic.ts: -------------------------------------------------------------------------------- 1 | import { Matrix, Tactic } from '../../classes/stix'; 2 | import { RenderableTechnique } from './renderable-technique'; 3 | import { ViewModel } from '../../classes'; 4 | 5 | export class RenderableTactic { 6 | public readonly tactic: Tactic; 7 | public readonly techniques: RenderableTechnique[] = []; 8 | public readonly subtechniques: RenderableTechnique[] = []; 9 | public height: number; 10 | 11 | constructor(tactic: Tactic, matrix: Matrix, viewModel: ViewModel, renderConfig: any) { 12 | this.tactic = tactic; 13 | let filteredTechniques = viewModel.sortTechniques(viewModel.filterTechniques(tactic.techniques, tactic, matrix), tactic); 14 | let yPosition = 1; // start at 1 to make space for tactic label 15 | for (let technique of filteredTechniques) { 16 | let techniqueVM = viewModel.getTechniqueVM(technique, tactic); 17 | let filteredSubtechniques = viewModel.filterTechniques(technique.subtechniques, tactic, matrix); 18 | 19 | let showSubtechniques = 20 | renderConfig.showSubtechniques == 'all' || (renderConfig.showSubtechniques == 'expanded' && techniqueVM.showSubtechniques); 21 | 22 | this.techniques.push(new RenderableTechnique(yPosition++, technique, tactic, matrix, viewModel, showSubtechniques)); 23 | 24 | if (filteredSubtechniques.length > 0 && showSubtechniques) { 25 | for (let subtechnique of filteredSubtechniques) { 26 | this.subtechniques.push(new RenderableTechnique(yPosition++, subtechnique, tactic, matrix, viewModel, renderConfig)); 27 | } 28 | } 29 | } 30 | this.height = yPosition; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /nav-app/src/app/svg-export/renderable-objects/renderable-technique.ts: -------------------------------------------------------------------------------- 1 | import { Matrix, Tactic, Technique } from '../../classes/stix'; 2 | import { TechniqueVM, ViewModel } from '../../classes'; 3 | import tinycolor from 'tinycolor2'; 4 | 5 | export class RenderableTechnique { 6 | public readonly yPosition: number; 7 | public readonly technique: Technique; 8 | public readonly tactic: Tactic; 9 | public readonly matrix: Matrix; 10 | public readonly showSubtechniques; 11 | public readonly viewModel: ViewModel; 12 | 13 | constructor(yPosition, technique: Technique, tactic: Tactic, matrix: Matrix, viewModel: ViewModel, showSubtechniques = false) { 14 | this.yPosition = yPosition; 15 | this.technique = technique; 16 | this.tactic = tactic; 17 | this.matrix = matrix; 18 | this.viewModel = viewModel; 19 | this.showSubtechniques = showSubtechniques; 20 | } 21 | 22 | public get fill() { 23 | if (this.viewModel.hasTechniqueVM(this.technique, this.tactic)) { 24 | let techniqueVM: TechniqueVM = this.viewModel.getTechniqueVM(this.technique, this.tactic); 25 | if (!techniqueVM.enabled) return 'white'; 26 | if (techniqueVM.color) return techniqueVM.color; 27 | if (this.viewModel.layout.showAggregateScores && techniqueVM.aggregateScoreColor) return techniqueVM.aggregateScoreColor; 28 | if (techniqueVM.score) return techniqueVM.scoreColor; 29 | } 30 | return null; //default 31 | } 32 | 33 | public get textColor() { 34 | if (this.viewModel.hasTechniqueVM(this.technique, this.tactic)) { 35 | let techniqueVM: TechniqueVM = this.viewModel.getTechniqueVM(this.technique, this.tactic); 36 | if (!techniqueVM.enabled) return '#aaaaaa'; 37 | } 38 | if (this.fill) { 39 | return tinycolor.mostReadable(this.fill, ['white', 'black']); //default; 40 | } 41 | return null; 42 | } 43 | 44 | public get text() { 45 | let text = []; 46 | if (this.viewModel.layout.showID) text.push(this.technique.attackID); 47 | if (this.viewModel.layout.showName) text.push(this.technique.name); 48 | return text.join(': '); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nav-app/src/app/svg-export/svg-export.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../colors.scss'; 2 | 3 | .svgcontainer { 4 | overflow-x: auto; 5 | text-align: center; 6 | min-height: 60vh; 7 | max-height: 60vh; 8 | min-width: 75vw; 9 | max-width: 75vw; 10 | padding: 25px; 11 | 12 | &.dark-mode { 13 | background-color: color(dark-1) !important; 14 | } 15 | } 16 | 17 | .dropdown-container { 18 | padding: 10px; 19 | top: 34px; 20 | ul { 21 | padding-left: 0; 22 | li { 23 | list-style: none; 24 | text-align: left; 25 | } 26 | } 27 | } 28 | 29 | .iewarning { 30 | color: red; 31 | } 32 | -------------------------------------------------------------------------------- /nav-app/src/app/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | // utilities for working with browser cookies 2 | 3 | /** 4 | * Set a cookie 5 | * @param {string} key key to set under 6 | * @param {string} value value to set under key 7 | * @param {number} expirationDays when cookie expires in days 8 | */ 9 | const setCookie = function (key: string, value: string, expirationDays: number) { 10 | let d = new Date(); 11 | d.setTime(d.getTime() + expirationDays * 24 * 60 * 60 * 1000); 12 | let expires = 'expires=' + d.toUTCString(); 13 | document.cookie = key + '=' + value + ';' + expires + ';path=/;SameSite=Strict'; 14 | }; 15 | 16 | /** 17 | * Get the value of the cookie under the given key 18 | * @param {string} key to retrieve from 19 | * @return {string} cookie value 20 | */ 21 | const getCookie = function (key: string): string { 22 | let name = key + '='; 23 | let decodedCookie = decodeURIComponent(document.cookie); 24 | let ca = decodedCookie.split(';'); 25 | 26 | for (let c of ca) { 27 | while (c.startsWith(' ')) { 28 | c = c.substring(1); 29 | } 30 | if (c.indexOf(name) == 0) { 31 | return c.substring(name.length, c.length); 32 | } 33 | } 34 | return ''; 35 | }; 36 | /** 37 | * Does a cookie exist under the given key? 38 | * @param {string} key to check 39 | * @return {boolean} true if cookie is stored under key, false otherwise 40 | */ 41 | const hasCookie = function (key: string): boolean { 42 | return getCookie(key) !== ''; 43 | }; 44 | 45 | /** 46 | * Delete the given cookie 47 | * @param {string} key to delete 48 | */ 49 | const deleteCookie = function (key: string) { 50 | document.cookie = key + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;SameSite=Strict'; 51 | }; 52 | 53 | export { setCookie, getCookie, hasCookie, deleteCookie }; 54 | -------------------------------------------------------------------------------- /nav-app/src/app/utils/globals.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import appPackageInfo from '../../../package.json'; 3 | 4 | export const navVersion: string = appPackageInfo.version; 5 | export const layerVersion: string = '4.5'; 6 | export const minimumSupportedVersion: string = '4.0'; 7 | -------------------------------------------------------------------------------- /nav-app/src/app/utils/navigator.d.ts: -------------------------------------------------------------------------------- 1 | // declaration merging workaround for msSaveOrOpenBlob function that was 2 | // marked for removal as a non-standard feature due to the removal of IE support 3 | // msSaveorOpenBlob removal issued: https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029 4 | // workaround pulled from: https://github.com/microsoft/TypeScript/issues/45612 5 | interface Navigator { 6 | msSaveOrOpenBlob: (blobOrBase64: Blob | string, filename: string) => boolean; 7 | } 8 | -------------------------------------------------------------------------------- /nav-app/src/app/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // utils.ts 2 | import { detect } from "detect-browser"; 3 | 4 | let comparatorFn = { 5 | '<': function(a, b) { return a < b; }, 6 | '<=': function(a, b) { return a <= b; }, 7 | '>': function(a, b) { return a > b; }, 8 | '>=': function(a, b) { return a >= b; } 9 | }; 10 | 11 | export function isBoolean(value: any): boolean { 12 | return typeof value === 'boolean'; 13 | } 14 | 15 | export function isNumber(value: any): boolean { 16 | return typeof value === 'number'; 17 | } 18 | 19 | export function isIE(): boolean { 20 | const browser = detect(); 21 | return browser.name == 'ie'; 22 | } 23 | 24 | export function isSafari(compRange): boolean { 25 | function compare(version, comp) { 26 | let str = (comp + ''); 27 | let n = +(/\d+/.exec(str) || NaN); 28 | let op = /^[<>]=?/.exec(str)[0]; 29 | return comparatorFn[op] ? comparatorFn[op](version, n) : (version == n || Number.isNaN(n)); 30 | } 31 | 32 | const browser = detect(); 33 | return browser.name == 'safari' && compare(browser.version.split('.')[0], compRange); 34 | } 35 | -------------------------------------------------------------------------------- /nav-app/src/app/version-upgrade/version-upgrade.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Upgrade layer version?

3 | 4 |

5 | The layer "{{ layerName }}" uses an outdated version of ATT&CK (v{{ vmVersion }}). Do you want to open the workflow to upgrade this 6 | layer to ATT&CK v{{ currVersion }}? 7 | 8 | This version is not supported by Navigator v{{ navVersion }} and must be upgraded for use. 9 | 10 |

11 |
12 | 13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /nav-app/src/app/version-upgrade/version-upgrade.component.scss: -------------------------------------------------------------------------------- 1 | .mat-dialog { 2 | text-align: center; 3 | .mat-mdc-dialog-actions { 4 | display: inline-block; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /nav-app/src/app/version-upgrade/version-upgrade.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { VersionUpgradeComponent } from './version-upgrade.component'; 4 | import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 5 | describe('VersionUpgradeComponent', () => { 6 | let component: VersionUpgradeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule, MatDialogModule], 12 | declarations: [VersionUpgradeComponent], 13 | providers: [ 14 | { 15 | provide: MatDialogRef, 16 | useValue: { 17 | close() { 18 | return {}; 19 | }, 20 | }, 21 | }, 22 | { 23 | provide: MAT_DIALOG_DATA, 24 | useValue: {}, 25 | }, 26 | ], 27 | }).compileComponents(); 28 | })); 29 | 30 | beforeEach(() => { 31 | fixture = TestBed.createComponent(VersionUpgradeComponent); 32 | component = fixture.componentInstance; 33 | component.data.currVersion = '4.9'; 34 | component.data.vmVersion = '4.8'; 35 | component.data.layerName = 'test1'; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | 43 | it('should upgrade version', () => { 44 | const openDialogSpy = spyOn(component.dialogRef, 'close'); 45 | component.upgradeVersion(true); 46 | expect(openDialogSpy).toHaveBeenCalledWith({ 47 | upgrade: true, 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /nav-app/src/app/version-upgrade/version-upgrade.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | import { ViewModelsService } from '../services/viewmodels.service'; 4 | import { DataService } from '../services/data.service'; 5 | import * as globals from '../utils/globals'; 6 | 7 | @Component({ 8 | selector: 'app-version-upgrade', 9 | templateUrl: './version-upgrade.component.html', 10 | styleUrls: ['./version-upgrade.component.scss'], 11 | providers: [ViewModelsService], 12 | }) 13 | export class VersionUpgradeComponent implements OnInit { 14 | navVersion = globals.navVersion; 15 | currVersion: string; 16 | vmVersion: string; 17 | layerName: string; 18 | 19 | constructor( 20 | public dialogRef: MatDialogRef, 21 | public dataService: DataService, 22 | private viewModelsService: ViewModelsService, 23 | @Inject(MAT_DIALOG_DATA) public data 24 | ) {} 25 | 26 | ngOnInit() { 27 | this.currVersion = this.data.currVersion; 28 | this.vmVersion = this.data.vmVersion; 29 | this.layerName = this.data.layerName; 30 | } 31 | 32 | upgradeVersion(upgrade: boolean) { 33 | this.dialogRef.close({ upgrade: upgrade }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitre-attack/attack-navigator/5f3c6ad136927b43cb15af330aa2858ababafebb/nav-app/src/assets/icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /nav-app/src/assets/icons/baseline-grid_on-24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_camera_alt_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_check_box_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_check_box_outline_blank_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_clear_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_clear_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_close_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_color_lens_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_content_copy_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_description_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_done_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_done_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_exportAllExcel_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_exportAllJson_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_exportJson_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_file_download_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_file_upload_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_file_upload_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_filter_list_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_format_color_fill_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_format_color_fill_black_nobottom_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_format_color_fill_gray_nobottom_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_format_size_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_insert_chart_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_insert_chart_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_insert_comment_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_insert_comment_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_keyboard_arrow_down_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_keyboard_arrow_right_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_keyboard_arrow_up_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_layers_clear_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_layers_clear_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_link_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_link_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_lock_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_lock_open_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_metadata_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_metadata_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_palette_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_photo_size_select_large_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_playlist_add_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_push_pin_black_24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_push_pin_gray.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_remove_circle_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_save_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_save_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_search_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_search_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_alphabetically_ascending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | A 7 | Z 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_alphabetically_ascending_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | A 7 | Z 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_alphabetically_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | A 7 | 8 | 9 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_alphabetically_descending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Z 7 | A 8 | 9 | 10 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_alphabetically_descending_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Z 7 | A 8 | 9 | 10 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_numerically_ascending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 1 7 | 2 8 | 9 | 10 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_numerically_ascending_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 1 7 | 2 8 | 9 | 10 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_numerically_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | # 7 | 8 | 9 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_numerically_descending.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 2 7 | 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_sort_numerically_descending_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 2 7 | 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_texture_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_texture_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_unfold_more_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_view_large_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_view_list_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_view_list_grey_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_view_medium_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_view_small_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_visibility_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_visibility_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/ic_visibility_off_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/table_view_FILL0_wght400_GRAD0_opsz24.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_less_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_less_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_more_alt_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_more_alt_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_more_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/icons/unfold_more_gray_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nav-app/src/assets/image_scoreVariableExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitre-attack/attack-navigator/5f3c6ad136927b43cb15af330aa2858ababafebb/nav-app/src/assets/image_scoreVariableExample.png -------------------------------------------------------------------------------- /nav-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /nav-app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | }; 9 | -------------------------------------------------------------------------------- /nav-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitre-attack/attack-navigator/5f3c6ad136927b43cb15af330aa2858ababafebb/nav-app/src/favicon.ico -------------------------------------------------------------------------------- /nav-app/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitre-attack/attack-navigator/5f3c6ad136927b43cb15af330aa2858ababafebb/nav-app/src/favicon.png -------------------------------------------------------------------------------- /nav-app/src/index.google-analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /nav-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /nav-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.log(err)); 14 | -------------------------------------------------------------------------------- /nav-app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es/symbol'; 23 | import 'core-js/es/object'; 24 | import 'core-js/es/function'; 25 | import 'core-js/es/parse-int'; 26 | import 'core-js/es/parse-float'; 27 | import 'core-js/es/number'; 28 | import 'core-js/es/math'; 29 | import 'core-js/es/string'; 30 | import 'core-js/es/date'; 31 | import 'core-js/es/array'; 32 | import 'core-js/es/regexp'; 33 | import 'core-js/es/map'; 34 | import 'core-js/es/weak-map'; 35 | import 'core-js/es/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es/reflect'; 42 | 43 | /** Evergreen browsers require these. **/ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | 46 | /** 47 | * Required to support Web Animations `@angular/platform-browser/animations`. 48 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 49 | **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | 52 | /*************************************************************************************************** 53 | * Zone JS is required by Angular itself. 54 | */ 55 | import 'zone.js'; // Included with Angular CLI. 56 | 57 | /*************************************************************************************************** 58 | * APPLICATION IMPORTS 59 | */ 60 | 61 | /** 62 | * Date, currency, decimal and percent pipes. 63 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 64 | */ 65 | // import 'intl'; // Run `npm install --save intl`. 66 | /** 67 | * Need to import at least one locale-data with intl. 68 | */ 69 | // import 'intl/locale-data/jsonp/en'; 70 | if (!Element.prototype.matches) { 71 | Element.prototype.matches = (Element.prototype).msMatchesSelector || Element.prototype.webkitMatchesSelector; 72 | } 73 | -------------------------------------------------------------------------------- /nav-app/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 9 | declare const __karma__: any; 10 | 11 | // Prevent Karma from running prematurely. 12 | __karma__.loaded = function () {}; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 16 | // Finally, start Karma to run the tests. 17 | __karma__.start(); 18 | -------------------------------------------------------------------------------- /nav-app/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /nav-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts", 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /nav-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "target": "ES2022", 10 | "typeRoots": [ 11 | "node_modules/@types" 12 | ], 13 | "lib": [ 14 | "es2017", 15 | "dom" 16 | ], 17 | "module": "es2020", 18 | "baseUrl": "./", 19 | "resolveJsonModule": true, 20 | "allowSyntheticDefaultImports": true, 21 | "useDefineForClassFields": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nav-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": ["src/test.ts", "src/polyfills.ts"], 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /nav-app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "deprecation": { 12 | "severity": "warning" 13 | }, 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "import-spacing": true, 16 | "indent": [true, "spaces"], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 25 | } 26 | ], 27 | "no-arg": true, 28 | "no-bitwise": true, 29 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 30 | "no-construct": true, 31 | "no-debugger": true, 32 | "no-duplicate-super": true, 33 | "no-empty": false, 34 | "no-empty-interface": true, 35 | "no-eval": true, 36 | "no-inferrable-types": [true, "ignore-params"], 37 | "no-misused-new": true, 38 | "no-non-null-assertion": true, 39 | "no-shadowed-variable": true, 40 | "no-string-literal": false, 41 | "no-string-throw": true, 42 | "no-switch-case-fall-through": true, 43 | "no-trailing-whitespace": true, 44 | "no-unnecessary-initializer": true, 45 | "no-unused-expression": true, 46 | "no-var-keyword": true, 47 | "object-literal-sort-keys": false, 48 | "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], 49 | "prefer-const": true, 50 | "quotemark": [true, "single"], 51 | "radix": true, 52 | "semicolon": [true, "always"], 53 | "triple-equals": [true, "allow-null-check"], 54 | "typedef-whitespace": [ 55 | true, 56 | { 57 | "call-signature": "nospace", 58 | "index-signature": "nospace", 59 | "parameter": "nospace", 60 | "property-declaration": "nospace", 61 | "variable-declaration": "nospace" 62 | } 63 | ], 64 | "typeof-compare": true, 65 | "unified-signatures": true, 66 | "variable-name": false, 67 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], 68 | "directive-selector": [true, "attribute", "app", "camelCase"], 69 | "component-selector": [true, "element", "app", "kebab-case"], 70 | "use-input-property-decorator": true, 71 | "use-output-property-decorator": true, 72 | "use-host-property-decorator": true, 73 | "no-input-rename": true, 74 | "no-output-rename": true, 75 | "use-life-cycle-interface": true, 76 | "use-pipe-transform-interface": true, 77 | "component-class-suffix": true, 78 | "directive-class-suffix": true, 79 | "invoke-injectable": true 80 | } 81 | } 82 | --------------------------------------------------------------------------------