├── .github └── workflows │ ├── lint.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .trunk ├── .gitignore ├── configs │ ├── .eslintrc.json │ ├── .markdownlint.yaml │ ├── .prettierignore │ ├── .prettierrc.json │ ├── .shellcheckrc │ └── .yamllint.yaml └── trunk.yaml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── SECURITY.md ├── docs └── sarif_explorer_spec.md ├── justfile ├── media ├── README │ ├── gif │ │ ├── .gitignore │ │ ├── browse_results.gif │ │ ├── classify_results.gif │ │ ├── copy_permalink.gif │ │ ├── filter_results.gif │ │ ├── open_gh_issue.gif │ │ ├── open_multiple_files.gif │ │ └── send_bugs_to_weaudit.gif │ ├── icons │ │ ├── .gitignore │ │ ├── bug.png │ │ ├── bug_nobg.png │ │ ├── bug_red.png │ │ ├── bug_red_nobg.png │ │ ├── check.png │ │ ├── check_green.png │ │ ├── check_green_nobg.png │ │ ├── check_nobg.png │ │ ├── collapse-all.png │ │ ├── collapse-all_nobg.png │ │ ├── filter.png │ │ ├── filter_nobg.png │ │ ├── folder.png │ │ ├── folder_nobg.png │ │ ├── github-alt.png │ │ ├── github-alt_nobg.png │ │ ├── link.png │ │ ├── link_nobg.png │ │ ├── question.png │ │ ├── question_nobg.png │ │ ├── refresh.png │ │ ├── refresh_nobg.png │ │ ├── repo-push.png │ │ └── repo-push_nobg.png │ ├── main.png │ └── main_cropped.png ├── banner-dark-mode.png ├── banner-light-mode.png ├── banner.svg ├── logo.png └── logo.svg ├── package-lock.json ├── package.json ├── scripts └── build_and_install.sh ├── src ├── extension │ ├── README.md │ ├── extension.ts │ ├── operations │ │ ├── handleSarifNotes.ts │ │ ├── openCodeRegion.ts │ │ └── openSarifFile.ts │ ├── sarifExplorerWebview.ts │ ├── weAuditInterface.ts │ └── weAuditTypes.ts ├── shared │ ├── README.md │ ├── file.ts │ ├── filterData.ts │ ├── resultTypes.ts │ └── webviewMessageTypes.ts ├── test │ ├── fakerule.yaml │ ├── runTest.ts │ ├── sarif_files │ │ ├── circomspect.sarif │ │ ├── clippy.sarif │ │ ├── codeql.sarif │ │ ├── empty_results.sarif │ │ ├── fake.sarif │ │ ├── local.sarif │ │ ├── multirun_test.sarif │ │ ├── semgrep.sarif │ │ └── semgrep2.sarif │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts └── webviewSrc │ ├── README.md │ ├── extensionApi.ts │ ├── main.html │ ├── main.ts │ ├── resizablePanels │ └── resizablePanels.ts │ ├── result │ ├── result.ts │ ├── resultDetailsWidget.ts │ ├── resultFilters.ts │ ├── resultsTable.ts │ └── resultsTableWidget.ts │ ├── sarifFile │ ├── sarifFile.ts │ ├── sarifFileDetailsWidget.ts │ ├── sarifFileList.ts │ └── sarifFileListWidget.ts │ ├── style.scss │ ├── tabs.ts │ ├── utils.ts │ └── vscode.ts ├── tsconfig.json └── webpack.config.js /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | lint: 16 | uses: trailofbits/.github/.github/workflows/lint.yml@v0.1.0 17 | with: 18 | type: just 19 | permissions: 20 | contents: read 21 | pull-requests: read 22 | checks: write 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build release and publish on VSCode's Marketplace and OpenVSX 2 | on: 3 | release: 4 | types: [published] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version-file: .nvmrc 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Build the vsix 25 | run: npx vsce package -o sarif-explorer.vsix 26 | 27 | - uses: actions/upload-artifact@v4 28 | with: 29 | name: sarif-explorer 30 | path: sarif-explorer.vsix 31 | 32 | upload-asset: 33 | runs-on: ubuntu-latest 34 | needs: build 35 | if: success() 36 | permissions: 37 | contents: write 38 | steps: 39 | - uses: actions/download-artifact@v4 40 | with: 41 | name: sarif-explorer 42 | 43 | - name: Add vsix to the release assets 44 | uses: actions/upload-release-asset@v1.0.2 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | with: 48 | upload_url: ${{ github.event.release.upload_url }} 49 | asset_path: sarif-explorer.vsix 50 | asset_name: sarif-explorer.vsix 51 | asset_content_type: application/octet-stream 52 | 53 | publish: 54 | runs-on: ubuntu-latest 55 | needs: build 56 | if: success() 57 | steps: 58 | - uses: actions/download-artifact@v4 59 | with: 60 | name: sarif-explorer 61 | 62 | - name: Publish Extension on VSCode's Marketplace 63 | run: npx vsce publish --pat ${{ secrets.VSCODE_PUBLISHING_TOKEN }} --packagePath sarif-explorer.vsix 64 | 65 | - name: Publish Extension on OpenVSX 66 | run: npx ovsx publish --pat ${{ secrets.OPENVSX_PUBLISHING_TOKEN }} --packagePath sarif-explorer.vsix 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .vscode-test/ 4 | .DS_Store 5 | 6 | src/test/sarif_files_dont_upload 7 | /codeql.db 8 | /codeql.sarif 9 | /semgrep.sarif 10 | 11 | *.sarifexplorer 12 | 13 | sarif-explorer.vsix -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.13.1 -------------------------------------------------------------------------------- /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | *logs 3 | *actions 4 | *notifications 5 | *tools 6 | plugins 7 | user_trunk.yaml 8 | user.yaml 9 | tmp 10 | -------------------------------------------------------------------------------- /.trunk/configs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": ["./tsconfig.json"] 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | // Disable 'no-unused-vars' for variables starting with `_` 11 | "no-unused-vars": "off", 12 | "@typescript-eslint/no-unused-vars": [ 13 | "warn", 14 | { 15 | "argsIgnorePattern": "^_", 16 | "varsIgnorePattern": "^_", 17 | "caughtErrorsIgnorePattern": "^_" 18 | } 19 | ] 20 | }, 21 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /.trunk/configs/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Autoformatter friendly markdownlint config (all formatting rules disabled) 2 | default: true 3 | # blank_lines: false 4 | # bullet: false 5 | # html: false 6 | # indentation: false 7 | line_length: false 8 | # spaces: false 9 | # url: false 10 | # whitespace: false 11 | 12 | MD007: 13 | # Spaces for indent 14 | indent: 2 15 | # Whether to indent the first level of the list 16 | start_indented: true 17 | # Spaces for first level indent (when start_indented is set) 18 | start_indent: 2 19 | 20 | MD032: false 21 | 22 | MD012: 23 | # Consecutive blank lines 24 | maximum: 2 25 | 26 | MD033: false 27 | -------------------------------------------------------------------------------- /.trunk/configs/.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | dist/ -------------------------------------------------------------------------------- /.trunk/configs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "tabWidth": 4, "printWidth": 120 } 2 | -------------------------------------------------------------------------------- /.trunk/configs/.shellcheckrc: -------------------------------------------------------------------------------- 1 | enable=all 2 | source-path=SCRIPTDIR 3 | disable=SC2154 4 | 5 | # If you're having issues with shellcheck following source, disable the errors via: 6 | # disable=SC1090 7 | # disable=SC1091 8 | -------------------------------------------------------------------------------- /.trunk/configs/.yamllint.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | quoted-strings: 3 | required: only-when-needed 4 | extra-allowed: ["{|}"] 5 | key-duplicates: {} 6 | octal-values: 7 | forbid-implicit-octal: true 8 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | # This file controls the behavior of Trunk: https://docs.trunk.io/cli 2 | # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml 3 | version: 0.1 4 | cli: 5 | version: 1.22.1 6 | # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) 7 | plugins: 8 | sources: 9 | - id: trunk 10 | ref: v1.5.0 11 | uri: https://github.com/trunk-io/plugins 12 | # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) 13 | runtimes: 14 | enabled: 15 | - go@1.21.0 16 | - python@3.10.8 17 | - node@20.11.1 18 | # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) 19 | lint: 20 | disabled: 21 | - oxipng 22 | enabled: 23 | - actionlint@1.7.0 24 | - checkov@3.2.91 25 | - eslint@8.57.0 26 | - git-diff-check 27 | - markdownlint@0.40.0 28 | - osv-scanner@1.7.3 29 | - prettier@3.2.5 30 | - shellcheck@0.10.0 31 | - shfmt@3.6.0 32 | - trivy@0.51.1 33 | - trufflehog@3.75.1 34 | - yamllint@1.35.1 35 | actions: 36 | disabled: 37 | - trunk-announce 38 | - trunk-check-pre-push 39 | - trunk-fmt-pre-commit 40 | enabled: 41 | - trunk-upgrade-available 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 23 | ], 24 | "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], 25 | "preLaunchTask": "tasks: watch-tests" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | */** 4 | 5 | !dist/ 6 | !LICENSE 7 | !README.md 8 | !package.json 9 | !media/logo.png 10 | !media/logo.svg 11 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Vasco-jofra @fcasal -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | # SARIF Explorer: Enjoy reviewing your static analysis results 11 | 12 | SARIF Explorer is a VSCode extension that enables you to review static analysis results effectively and enjoyably. No more browsing `.txt` or `.csv` files. 13 | 14 | 15 | Whether you are a developer or a code auditor, SARIF Explorer allows you to classify your tool's results as `Bug` or `False Positive`, add comments to the results, export the bugs you triaged, and much more (see [Features](#features) below). 16 | 17 |  18 | 19 | 20 | ## Installation 21 | 22 | Install the extension by searching for [SARIF Explorer](https://marketplace.visualstudio.com/items?itemName=trailofbits.sarif-explorer) in the VSCode Extensions browser. See the [Build and install](#development) section below for how to build and install from code. 23 | 24 | 25 | ## Features 26 | 27 | - [**Open Multiple SARIF Files**](#open-multiple-sarif-files): Open and browse the results of multiple SARIF files simultaneously. 28 | - [**Browse Results**](#browse-results): Browse results by clicking on them, which will open their associated location in VSCode. You can also browse a result's dataflow steps, if present. 29 | - [**Classify Results**](#classify-results): Add metadata to each result by classifying them as a `Bug`, `False Positive`, or `Todo`, and adding a custom text comment. 30 | - [**Filter Results**](#filter-results): Filter results by keyword, path (to include or exclude), level (`error`, `warning`, `note`, or `none`), and status (`Bug`, `False Positive`, or `Todo`). You can also hide all results from a specific SARIF file or from a specific rule. 31 | - [**Copy GitHub Permalinks**](#copy-github-permalinks): Copy a GitHub permalink to the location associated with a result. Requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 32 | - [**Create GitHub Issues**](#create-github-issues): Create formatted GitHub issues for a specific result or for all the un-filtered results under a given rule. Requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 33 | - [**Send Bugs to weAudit**](#send-bugs-to-weaudit): Send all results classified as `Bug` to [weAudit](https://github.com/trailofbits/vscode-weaudit) (results are automatically de-duplicated). Requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 34 | - [**Collaborate**](#collaborate): Share the `.sarifexplorer` file with your colleagues (e.g., on GitHub) to share your comments and classified results. 35 | 36 | 37 | ## Suggested Work Flow 38 | 39 | 1. Run all of your static analysis tools and store the resulting SARIF files in the folder where you ran them 40 | 2. Open SARIF Explorer and open all the SARIF files 41 | 3. Filter out the noisy results: 42 | - Are there rules that you are not interested in seeing? Hide them! 43 | - Are there folders for which you don't care about the results (e.g., the `./tests` folder)? Filter them out! 44 | 4. Triage the results: 45 | - determine if each result is a false positive or a bug 46 | - swipe left or right accordingly (i.e., click the left or right arrow) 47 | - add additional context with a comment if necessary 48 | 5. Working with other team members? Share your progress by committing the [`.sarifexplorer` file](./docs/sarif_explorer_spec.md) 49 | 6. Send all results marked as bugs to [weAudit](https://github.com/trailofbits/vscode-weaudit) and proceed with the [weAudit](https://github.com/trailofbits/vscode-weaudit) workflow 50 | 51 | 52 | ## Concepts 53 | 54 | - **SARIF Files**: The SARIF files you've opened that can be viewed in the `SARIF Files` tab. 55 | - **Results**: The results loaded from the SARIF Files that can be viewed in the `Results` tab. 56 | - **Base Folder**: The absolute path of the folder against which you ran your static analysis tool. SARIF Explorer uses this path to know where to open a result's associated code. In most situations, SARIF Explorer's heuristics will automatically find this folder for you. 57 | 58 | 59 | ## Keybindings 60 | 61 | In the `Results` tab: 62 | - `ArrowDown`: Select the result below 63 | - `ArrowUp`: Select the result above 64 | - `ArrowRight`: Classify the selected result as a `Bug` and select the result below 65 | - `ArrowLeft`: Classify the selected result as a `False Positive` and select the result below 66 | - `Backspace`: Classify the selected result as `Todo` and select the result below 67 | 68 | 69 | --- 70 | 71 | 72 | ### Open Multiple SARIF Files 73 | 74 | Open multiple files by clicking the button in the top bar and selecting multiple SARIF files. You can browse the list of opened SARIF files in the `SARIF Files` tab, where you can also close or reload a given SARIF file. 75 | 76 | In the detailed view of the SARIF file, you can see its full path, the number of results it found, and which rules it ran on the code even if no results were found with that rule (if the tool produces a correct SARIF file). In this view, you can also modify the [Base Folder](#concepts) associated with the SARIF file. 77 | 78 |  79 | 80 | Opening a file with the `.sarif` extension in VSCode will also trigger SARIF Explorer to open it and show its results. 81 | 82 | 83 | ### Browse Results 84 | 85 | Browse all the opened results in the `Results` tab by opening a rule and clicking on a result. This will open the code location associated with the result. 86 | 87 | In the detailed view of the result, you have more detailed information, including data flow data which you can browse from source to sink. 88 | 89 |  90 | 91 | 92 | ### Classify Results 93 | 94 | Classify a result with your mouse or with keyboard shortcuts. 95 | 96 | **Using the mouse**: With a result selected, click the button to classify it as a `Bug`, the button to classify it as a `False Positive`, and the button to reset the classification to `Todo`. These buttons appear next to the result and in the result's detailed view. 97 | 98 | **Using the keyboard**: To be more efficient, select a result and press the `ArrowRight` key to classify it as a `Bug`, the `ArrowLeft` key to classify it as a `False Positive`, and the `Backspace` key to reset the classification to `Todo`. 99 | 100 |  101 | 102 | 103 | ### Filter Results 104 | 105 | Filter by keywords by typing in the filter area in the top bar. The keyword search is case insensitive and matches against the result's display path, line number, message, associated SARIF file, comment, rule name, rule description, and the name of the tool that generated the result. 106 | 107 | For more filtering options, open the filter menu by clicking the button in the top bar. Inside the filter menu, you have options to: 108 | - Filter by paths including or excluding a keyword (you can use multiple paths separated by commas) 109 | - Filter by the result's level (`error`, `warning`, `note`, or `none`) 110 | - Filter by the result's status (`Todo`, `Bug`, or `False Positive`) 111 | 112 | Example: you want to remove all results from the `tests` and `third_party` folders, and to see only results classified as `Todo`. You should: 113 | - set `Exclude Paths Containing` to `/tests/, /third_party`, and 114 | - check the `Todo` box and uncheck the `Bug` and `False Positive` boxes in the `Status` section 115 | 116 | NOTE: Filters do not get re-applied automatically when a result is updated; you need to click the button to refresh the filters. This design was chosen to prevent the UI from jumping around when you are classifying results or adding comments. 117 | 118 |  119 | 120 | 121 | ### Copy GitHub Permalinks 122 | 123 | Copy a GitHub permalink to the location associated with the result. Do this by clicking the button next to a result or in the result's detailed view. 124 | 125 | The permalink target repository will be chosen according to your [weAudit](https://github.com/trailofbits/vscode-weaudit) configuration. This feature requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 126 | 127 |  128 | 129 | 130 | ### Create GitHub Issues 131 | 132 | Create a GitHub issue with data about your results. You can create two kinds of GitHub issues: 133 | 1. An issue for a specific result by clicking on the button next to a result or in a result's detailed view. 134 | 2. An issue for a group of results by clicking on the button next to a rule in the result table. The issue will include all the results under the rule that are not filtered out (not just those classified as `Bug`). 135 | 136 | The GitHub issues will be created in a repository according to your [weAudit](https://github.com/trailofbits/vscode-weaudit) configuration. This feature requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 137 | 138 |  139 | 140 | 141 | ### Send Bugs to weAudit 142 | 143 | Send all results classified as `Bug` to [weAudit](https://github.com/trailofbits/vscode-weaudit) by clicking the button in the top bar. Results are automatically de-duplicated (on the [weAudit](https://github.com/trailofbits/vscode-weaudit) side); so, if you classify a new `Bug` and resend all `Bug`s again, only the new one will be added. 144 | 145 | For obvious reasons, this feature requires having [weAudit](https://github.com/trailofbits/vscode-weaudit) installed. 146 | 147 |  148 | 149 | 150 | ### Collaborate 151 | 152 | Share the `.sarifexplorer` file with your colleagues (e.g., on GitHub) to share your comments and classified results. The file is a prettified JSON file, which helps resolve conflicts if more than one person writes to the file in parallel. 153 | 154 | 155 | ## Development 156 | 157 | ### Build and install 158 | 159 | To build and install a new vsix file run the following script: 160 | 161 | ```bash 162 | npm install 163 | ./scripts/build_and_install.sh 164 | ``` 165 | 166 | ### Architecture 167 | 168 | The extension has two parts: the extension--the privileged part that can read files from the filesystem and execute arbitrary nodeJS--, and the Webview--the unprivileged part responsible for drawing the UI. These two parts communicate with `postMessage`. Their code is split into different folders, which both include a README with an explanation of their purpose. 169 | 170 | ### SARIF Explorer file format 171 | 172 | The SARIF explorer file format is detailed in [sarif_explorer_spec.md](./docs/sarif_explorer_spec.md). 173 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/trailofbits/vscode-sarif-explorer/security/) tab. 6 | 7 | After the initial reply to your report, we will keep you informed of the progress toward a fix and full announcement, and may ask for additional information or guidance. 8 | 9 | Report security bugs in third-party modules to the person or team maintaining the module. 10 | -------------------------------------------------------------------------------- /docs/sarif_explorer_spec.md: -------------------------------------------------------------------------------- 1 | # Specification of the .sarifexplorer file 2 | 3 | Example: 4 | 5 | ```json 6 | { 7 | "resultIdToNotes": { 8 | "0|23": { 9 | "status": 1, 10 | "comment": "This is a False Positive because..." 11 | }, 12 | "0|24": { 13 | "status": 2, 14 | "comment": "This is a bug because attacker-controlled data can reach it in [this] way" 15 | } 16 | }, 17 | "hiddenRules": [ 18 | "python.lang.security.audit.insecure-file-permissions.insecure-file-permissions" 19 | ] 20 | } 21 | ``` 22 | 23 | The `.sarifexplorer` format is a `.json` file with two objects: 24 | - The `resultIdToNotes` dictionary 25 | - The `hiddenRules` array 26 | 27 | The file is stored well formatted to ease diffing and merging of conflicts in git. 28 | 29 | 30 | ## resultIdToNotes 31 | 32 | A dictionary of result id to metadata about a result, which includes the results status (`Bug`, `False Positive`, or `Todo`) and any text note that a user may have added. If no modifications were made to a result, the result will have no entry in this dictionary. 33 | 34 | A result id consists of the concatenation of: 35 | - the result's `runIndex` (usually just `0`) 36 | - the result's index within the run 37 | 38 | CAREFUL: If the SARIF file is updated after you triage some results (e.g., by re-running the tool with more rules) these indexes may become desynchronized. If updating a SARIF file while keeping the notes consistent is a feature that users want, we'll need to implement this functionality in the future. In the past, we've tried to store the result's fingerprint when present (instead of the result's index within the run). This didn't work because many tools emitted multiple results with the same fingerprint, which resulted in these results having the same id. 39 | 40 | 41 | ## hiddenRules 42 | 43 | The `hiddenRules` array contains a list of rules that are hidden in the UI. 44 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | dev: 2 | npm install 3 | -------------------------------------------------------------------------------- /media/README/gif/.gitignore: -------------------------------------------------------------------------------- 1 | *.mov 2 | old -------------------------------------------------------------------------------- /media/README/gif/browse_results.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/browse_results.gif -------------------------------------------------------------------------------- /media/README/gif/classify_results.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/classify_results.gif -------------------------------------------------------------------------------- /media/README/gif/copy_permalink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/copy_permalink.gif -------------------------------------------------------------------------------- /media/README/gif/filter_results.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/filter_results.gif -------------------------------------------------------------------------------- /media/README/gif/open_gh_issue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/open_gh_issue.gif -------------------------------------------------------------------------------- /media/README/gif/open_multiple_files.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/open_multiple_files.gif -------------------------------------------------------------------------------- /media/README/gif/send_bugs_to_weaudit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/gif/send_bugs_to_weaudit.gif -------------------------------------------------------------------------------- /media/README/icons/.gitignore: -------------------------------------------------------------------------------- 1 | *.svg -------------------------------------------------------------------------------- /media/README/icons/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/bug.png -------------------------------------------------------------------------------- /media/README/icons/bug_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/bug_nobg.png -------------------------------------------------------------------------------- /media/README/icons/bug_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/bug_red.png -------------------------------------------------------------------------------- /media/README/icons/bug_red_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/bug_red_nobg.png -------------------------------------------------------------------------------- /media/README/icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/check.png -------------------------------------------------------------------------------- /media/README/icons/check_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/check_green.png -------------------------------------------------------------------------------- /media/README/icons/check_green_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/check_green_nobg.png -------------------------------------------------------------------------------- /media/README/icons/check_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/check_nobg.png -------------------------------------------------------------------------------- /media/README/icons/collapse-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/collapse-all.png -------------------------------------------------------------------------------- /media/README/icons/collapse-all_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/collapse-all_nobg.png -------------------------------------------------------------------------------- /media/README/icons/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/filter.png -------------------------------------------------------------------------------- /media/README/icons/filter_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/filter_nobg.png -------------------------------------------------------------------------------- /media/README/icons/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/folder.png -------------------------------------------------------------------------------- /media/README/icons/folder_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/folder_nobg.png -------------------------------------------------------------------------------- /media/README/icons/github-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/github-alt.png -------------------------------------------------------------------------------- /media/README/icons/github-alt_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/github-alt_nobg.png -------------------------------------------------------------------------------- /media/README/icons/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/link.png -------------------------------------------------------------------------------- /media/README/icons/link_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/link_nobg.png -------------------------------------------------------------------------------- /media/README/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/question.png -------------------------------------------------------------------------------- /media/README/icons/question_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/question_nobg.png -------------------------------------------------------------------------------- /media/README/icons/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/refresh.png -------------------------------------------------------------------------------- /media/README/icons/refresh_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/refresh_nobg.png -------------------------------------------------------------------------------- /media/README/icons/repo-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/repo-push.png -------------------------------------------------------------------------------- /media/README/icons/repo-push_nobg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/icons/repo-push_nobg.png -------------------------------------------------------------------------------- /media/README/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/main.png -------------------------------------------------------------------------------- /media/README/main_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/README/main_cropped.png -------------------------------------------------------------------------------- /media/banner-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/banner-dark-mode.png -------------------------------------------------------------------------------- /media/banner-light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/banner-light-mode.png -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailofbits/vscode-sarif-explorer/ad5d110f60fc4d51edba3d5950ddfdf280e0cd78/media/logo.png -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sarif-explorer", 3 | "displayName": "SARIF Explorer", 4 | "description": "SARIF Explorer: Explore static analysis results effectively and enjoyably.", 5 | "version": "1.2.9", 6 | "publisher": "trailofbits", 7 | "author": { 8 | "name": "Trail of Bits" 9 | }, 10 | "license": "SEE LICENSE IN LICENSE", 11 | "homepage": "https://www.trailofbits.com/", 12 | "icon": "media/logo.png", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/trailofbits/vscode-sarif-explorer" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/trailofbits/vscode-sarif-explorer/issues" 19 | }, 20 | "categories": [ 21 | "Visualization", 22 | "Other" 23 | ], 24 | "keywords": [ 25 | "sarif", 26 | "static analysis", 27 | "auditing" 28 | ], 29 | "engines": { 30 | "vscode": "^1.78.0" 31 | }, 32 | "activationEvents": [ 33 | "onLanguage:json", 34 | "onStartupFinished" 35 | ], 36 | "main": "./dist/extension.js", 37 | "contributes": { 38 | "commands": [ 39 | { 40 | "command": "sarif-explorer.showSarifExplorer", 41 | "title": "SARIF Explorer: Show SARIF Explorer" 42 | }, 43 | { 44 | "command": "sarif-explorer.openSarifFile", 45 | "title": "SARIF Explorer: Open SARIF File" 46 | }, 47 | { 48 | "command": "sarif-explorer.resetWorkspaceData", 49 | "title": "SARIF Explorer: Reset Workspace Data" 50 | } 51 | ], 52 | "configuration": [ 53 | { 54 | "order": 1, 55 | "properties": { 56 | "sarif-explorer.showFullPathInResultsTable": { 57 | "type": "boolean", 58 | "default": false, 59 | "description": "Show the full path of a result in SARIF Explorer's result table." 60 | } 61 | } 62 | } 63 | ], 64 | "languages": [ 65 | { 66 | "id": "json", 67 | "extensions": [ 68 | ".sarif" 69 | ] 70 | } 71 | ] 72 | }, 73 | "scripts": { 74 | "vscode:prepublish": "npm run package", 75 | "vscode:package": "vsce package", 76 | "compile": "webpack --mode development", 77 | "watch": "webpack --mode development --watch", 78 | "package": "webpack --mode production --devtool hidden-source-map", 79 | "compile-tests": "tsc -p . --outDir out", 80 | "watch-tests": "tsc -p . -w --outDir out", 81 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 82 | "lint": "eslint --ext ts ./src", 83 | "prettify": "trunk fmt", 84 | "test": "node ./out/test/runTest.js" 85 | }, 86 | "devDependencies": { 87 | "@types/glob": "^8.0.0", 88 | "@types/mocha": "^10.0.1", 89 | "@types/node": "^18.11.18", 90 | "@types/vscode": "^1.74.0", 91 | "@types/webpack": "^5.28.0", 92 | "@typescript-eslint/eslint-plugin": "^5.47.1", 93 | "@typescript-eslint/parser": "^5.47.1", 94 | "@vscode/test-electron": "^2.2.1", 95 | "@vscode/vsce": "^3.0.0", 96 | "css-loader": "^6.8.1", 97 | "eslint": "^8.30.0", 98 | "glob": "^8.0.3", 99 | "mocha": "^10.2.0", 100 | "prettier": "^2.8.1", 101 | "sass": "^1.57.1", 102 | "sass-loader": "^13.2.0", 103 | "style-loader": "^3.3.1", 104 | "ts-loader": "^9.4.2", 105 | "ts-node": "^10.9.1", 106 | "typescript": "^4.9.4", 107 | "webpack": "^5.75.0", 108 | "webpack-cli": "^5.0.1" 109 | }, 110 | "dependencies": { 111 | "@vscode/codicons": "0.0.32" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /scripts/build_and_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build the extension (you might need to run `npm install` first) 4 | npm run vscode:package 5 | 6 | # Rename the extension to sarif-explorer.vsix 7 | rm sarif-explorer.vsix 8 | mv sarif-explorer-*.vsix sarif-explorer.vsix 9 | 10 | # Install the extension 11 | code --install-extension sarif-explorer.vsix 12 | -------------------------------------------------------------------------------- /src/extension/README.md: -------------------------------------------------------------------------------- 1 | # Extension source code 2 | 3 | This folder contains the extension's source code. It includes code that: 4 | - [Registers VSCode commands](./extension.ts) 5 | - [Creates and communicates with the Webview](./sarifExplorerWebview.ts) 6 | - Performs operations on behalf of the Webview: [opening a SARIF file](./operations/openSarifFile.ts), [opening a code region in VSCode](./operations/openCodeRegion.ts), and [persisting data about the SARIF file triage](./operations/handleSarifNotes.ts) 7 | 8 | This code is compiled with webpack and outputted to `dist/extension.js` (configured in [webpack.config.js](../../webpack.config.js)). 9 | -------------------------------------------------------------------------------- /src/extension/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { SarifExplorerWebview } from "./sarifExplorerWebview"; 3 | 4 | // This method is called when your extension is activated 5 | export function activate(context: vscode.ExtensionContext) { 6 | const sarifExplorer = new SarifExplorerWebview(context); 7 | 8 | context.subscriptions.push( 9 | vscode.commands.registerCommand("sarif-explorer.showSarifExplorer", () => { 10 | sarifExplorer.show(); 11 | }), 12 | ); 13 | 14 | context.subscriptions.push( 15 | vscode.commands.registerCommand("sarif-explorer.openSarifFile", (sarifPath: string, baseFolder: string) => { 16 | if (sarifPath) { 17 | sarifExplorer.addSarifToToOpenList(sarifPath, baseFolder); 18 | sarifExplorer.show(); 19 | } else { 20 | sarifExplorer.show(); 21 | sarifExplorer.launchOpenSarifFileDialogAndSendToWebview(); 22 | } 23 | }), 24 | ); 25 | 26 | context.subscriptions.push( 27 | vscode.commands.registerCommand("sarif-explorer.resetWorkspaceData", () => { 28 | // This command is useful if SARIF Explorer gets stuck in a bad state 29 | sarifExplorer.resetWorkspaceData(); 30 | }), 31 | ); 32 | 33 | // load a SARIF file when it is opened 34 | context.subscriptions.push( 35 | vscode.workspace.onDidOpenTextDocument(async (document) => { 36 | if (document.fileName.endsWith(".sarif")) { 37 | sarifExplorer.addSarifToToOpenList(document.fileName); 38 | sarifExplorer.show(); 39 | } 40 | }), 41 | ); 42 | 43 | // When we're loading for the first time, we need to check if there are SARIF files open 44 | // (otherwise, the extension would not automatically open the webview because the onDidOpenTextDocument 45 | // handler above was still not registered) 46 | let shouldShowWebview = false; 47 | vscode.workspace.textDocuments.forEach(async (document) => { 48 | if (document.fileName.endsWith(".sarif")) { 49 | sarifExplorer.addSarifToToOpenList(document.fileName); 50 | shouldShowWebview = true; 51 | } 52 | }); 53 | if (shouldShowWebview) { 54 | sarifExplorer.show(); 55 | } 56 | } 57 | 58 | // This method is called when your extension is deactivated 59 | // eslint-disable-next-line @typescript-eslint/no-empty-function 60 | export function deactivate() {} 61 | -------------------------------------------------------------------------------- /src/extension/operations/handleSarifNotes.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from "fs"; 2 | import { ResultNote, ResultNotes } from "../../shared/resultTypes"; 3 | 4 | export type SarifFileWorkspaceData = { 5 | baseFolder: string; 6 | }; 7 | 8 | export class SarifFileMetadata { 9 | private sarifFilePath: string; 10 | private notesFilePath: string; 11 | private resultIdToNotes: Map = new Map(); 12 | private hiddenRules: Set = new Set(); 13 | private workspaceMetadata: SarifFileWorkspaceData; 14 | 15 | constructor(sarifFilePath: string, sarifFileWorkspaceData: SarifFileWorkspaceData) { 16 | this.sarifFilePath = sarifFilePath; 17 | this.notesFilePath = this.sarifFilePath + ".sarifexplorer"; 18 | this.workspaceMetadata = sarifFileWorkspaceData; 19 | 20 | this.loadFromFile(); 21 | } 22 | 23 | public getSarifFilePath(): string { 24 | return this.sarifFilePath; 25 | } 26 | 27 | public getResultNotes(): ResultNotes { 28 | const resultNotes: ResultNotes = {}; 29 | for (const [resultId, note] of this.resultIdToNotes) { 30 | resultNotes[resultId] = note; 31 | } 32 | return resultNotes; 33 | } 34 | 35 | public getHiddenRules(): string[] { 36 | return Array.from(this.hiddenRules); 37 | } 38 | 39 | public getWorkspaceMetadata(): SarifFileWorkspaceData { 40 | return this.workspaceMetadata; 41 | } 42 | 43 | public getBaseFolder(): string { 44 | return this.workspaceMetadata.baseFolder; 45 | } 46 | 47 | public async setResultNote(resultId: string, note: ResultNote) { 48 | this.resultIdToNotes.set(resultId, note); 49 | 50 | this.writeFile(); 51 | } 52 | 53 | public async setHiddenRule(ruleId: string, isHidden: boolean) { 54 | if (isHidden) { 55 | this.hiddenRules.add(ruleId); 56 | } else { 57 | this.hiddenRules.delete(ruleId); 58 | } 59 | 60 | this.writeFile(); 61 | } 62 | 63 | public setBaseFolder(baseFolder: string) { 64 | this.workspaceMetadata.baseFolder = baseFolder; 65 | } 66 | 67 | public loadFromFile() { 68 | if (!existsSync(this.notesFilePath)) { 69 | return; 70 | } 71 | 72 | let res = JSON.parse(readFileSync(this.notesFilePath, "utf8")); 73 | if (typeof res === "string") { 74 | // We have to call JSON.parse again to ensure that 'res' is an object and not a string :thanks_js: 75 | // https://stackoverflow.com/questions/42494823/json-parse-returns-string-instead-of-object 76 | res = JSON.parse(res); 77 | } 78 | 79 | if (res.resultIdToNotes) { 80 | this.resultIdToNotes = new Map(Object.entries(res.resultIdToNotes)); 81 | } 82 | 83 | if (res.hiddenRules) { 84 | this.hiddenRules = new Set(res.hiddenRules); 85 | } 86 | } 87 | 88 | private async writeFile() { 89 | const objAsStr = JSON.stringify( 90 | { 91 | resultIdToNotes: this.getResultNotes(), 92 | hiddenRules: this.getHiddenRules(), 93 | }, 94 | null, 95 | 2, 96 | ); 97 | 98 | writeFileSync(this.notesFilePath, objAsStr); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/extension/operations/openCodeRegion.ts: -------------------------------------------------------------------------------- 1 | import { ResultRegion } from "../../shared/resultTypes"; 2 | import * as vscode from "vscode"; 3 | import * as path from "path"; 4 | import * as fs from "fs"; 5 | import { fileURLToPath } from "url"; 6 | 7 | export class SarifResultPathIsAbsolute extends Error { 8 | constructor(filePath: string) { 9 | super( 10 | `Result path is absolute but does not exist (${filePath}). ` + 11 | "The SARIF file must have been generated on another machine or moved from its original location. " + 12 | "As a workaround, you can open the SARIF file in your text editor and replace the old absolute path prefix with the path where you have the code on your machine", 13 | ); 14 | this.name = "SarifResultPathIsAbsolute"; 15 | } 16 | } 17 | 18 | export class BaseFolderIsIncorrectError extends Error { 19 | constructor(filePath: string, resultsBaseFolder: string) { 20 | super( 21 | `The user-provided base path ('${resultsBaseFolder}') plus the result's relative path ('${filePath}') does not exist`, 22 | ); 23 | this.name = "BaseFolderIsIncorrectError"; 24 | } 25 | } 26 | 27 | export class BaseFolderCouldNotBeDeterminedError extends Error { 28 | constructor(filePath: string) { 29 | super(`Could not determine the result's base folder based on several heuristics for the path '${filePath}'`); 30 | this.name = "BaseFolderCouldNotBeDeterminedError"; 31 | } 32 | } 33 | 34 | export class BaseFolderSelectionError extends Error { 35 | constructor(message: string) { 36 | super(message); 37 | this.name = "BaseFolderSelectionError"; 38 | } 39 | } 40 | 41 | type UriAndBaseFolder = { 42 | uri: vscode.Uri; 43 | baseFolder: string; 44 | }; 45 | 46 | // Reveal a code region in a file 47 | export async function openCodeRegion( 48 | resultPath: string, 49 | region: ResultRegion, 50 | baseFolder: string, 51 | potentialBaseFolders: string[], 52 | ): Promise { 53 | // get file uri associated with resultPath 54 | const resultFileUriAndBaseFolder = await getResultFileUri(resultPath, baseFolder, potentialBaseFolders); 55 | 56 | // open file with the given range 57 | openResource(resultFileUriAndBaseFolder.uri, region); 58 | 59 | return resultFileUriAndBaseFolder.baseFolder; 60 | } 61 | 62 | export async function getResultFileUri( 63 | filePath: string, 64 | resultsBaseFolder: string, 65 | potentialBaseFolders: string[], 66 | ): Promise { 67 | // Gets the path from the file URI (needed on Windows to replace `/` with `\` and more) 68 | if (filePath.startsWith("file://")) { 69 | filePath = fileURLToPath(filePath); 70 | } 71 | 72 | // If running on WSL, replace `C:` or `/C:` with `/mnt/c` 73 | const isRunningOnWSL = vscode.env.remoteName === "wsl"; 74 | if (isRunningOnWSL) { 75 | filePath = filePath.replace(/^\/?([A-Za-z]):\//, "/mnt/$1/"); 76 | } 77 | 78 | if (path.isAbsolute(filePath)) { 79 | // if the file is absolute and it exists, open it 80 | if (!fs.existsSync(filePath)) { 81 | // TODO: We could have a complex way to allow these SARIF files to became relative if necessary 82 | throw new SarifResultPathIsAbsolute(filePath); 83 | } 84 | 85 | return { 86 | uri: vscode.Uri.file(filePath), 87 | baseFolder: resultsBaseFolder, 88 | }; 89 | } 90 | 91 | // The path is relative 92 | if (resultsBaseFolder !== "") { 93 | // Relative and a base directory was provided, open it relative to the base directory 94 | const absolutePath = path.join(resultsBaseFolder, filePath); 95 | if (fs.existsSync(absolutePath)) { 96 | return { 97 | uri: vscode.Uri.file(absolutePath), 98 | baseFolder: resultsBaseFolder, 99 | }; 100 | } else { 101 | throw new BaseFolderIsIncorrectError(filePath, resultsBaseFolder); 102 | } 103 | } 104 | 105 | // Try to find the base folder using heuristics 106 | const heuristicUriAndBaseFolder = findBaseFolderWithHeuristics(filePath, potentialBaseFolders); 107 | if (heuristicUriAndBaseFolder) { 108 | return heuristicUriAndBaseFolder; 109 | } 110 | 111 | // If nothing else worked, ask the user to provide base folder 112 | throw new BaseFolderCouldNotBeDeterminedError(filePath); 113 | } 114 | 115 | function findBaseFolderWithHeuristics(filePath: string, potentialBaseFolders: string[]): UriAndBaseFolder | undefined { 116 | // Check based on other SARIF file's base folder 117 | for (const folder of potentialBaseFolders) { 118 | const absolutePath = path.join(folder, filePath); 119 | if (fs.existsSync(absolutePath)) { 120 | return { 121 | uri: vscode.Uri.file(absolutePath), 122 | baseFolder: folder, 123 | }; 124 | } 125 | } 126 | 127 | // Try the currently open workspace folders 128 | if (vscode.workspace.workspaceFolders !== undefined) { 129 | for (const workspaceFolder of vscode.workspace.workspaceFolders) { 130 | const workspaceFolderPath = workspaceFolder.uri.fsPath; 131 | const absolutePath = path.join(workspaceFolderPath, filePath); 132 | if (fs.existsSync(absolutePath)) { 133 | return { 134 | uri: vscode.Uri.file(absolutePath), 135 | baseFolder: workspaceFolderPath, 136 | }; 137 | } 138 | } 139 | } 140 | 141 | return undefined; 142 | } 143 | 144 | export async function openBaseFolderDialog(filePath: string): Promise { 145 | const baseFolder = await vscode.window.showOpenDialog({ 146 | defaultUri: vscode.workspace.workspaceFolders?.at(0)?.uri || undefined, 147 | canSelectFiles: false, 148 | canSelectFolders: true, 149 | canSelectMany: false, 150 | openLabel: "Select base folder for SARIF results", 151 | }); 152 | 153 | if (!baseFolder || baseFolder.length === 0) { 154 | throw new BaseFolderSelectionError("Unable to select base folder"); 155 | } 156 | 157 | const baseFolderUri = baseFolder[0]; 158 | 159 | const absolutePath = path.join(baseFolderUri.fsPath, filePath); 160 | if (!fs.existsSync(absolutePath)) { 161 | throw new BaseFolderSelectionError(`Path "${absolutePath}" does not exist`); 162 | } 163 | 164 | return { 165 | uri: vscode.Uri.file(absolutePath), 166 | baseFolder: baseFolderUri.fsPath, 167 | }; 168 | } 169 | 170 | // ==================== 171 | // Showing the region-related functions 172 | // If document is already open, it should not be opened again. Instead, the selection should be changed. 173 | function openResource(resource: vscode.Uri, region: ResultRegion): void { 174 | const vscodeRegion = regionToVscodeRegion(region); 175 | if ( 176 | vscode.window.activeTextEditor === undefined || 177 | (vscode.window.activeTextEditor !== undefined && 178 | vscode.window.activeTextEditor.document.fileName !== resource.fsPath) 179 | ) { 180 | vscode.window.showTextDocument(resource, { 181 | viewColumn: vscode.ViewColumn.One, 182 | selection: vscodeRegion, 183 | preserveFocus: true, 184 | }); 185 | } else { 186 | vscode.window.activeTextEditor.revealRange(vscodeRegion, vscode.TextEditorRevealType.InCenterIfOutsideViewport); 187 | vscode.window.activeTextEditor.selection = new vscode.Selection(vscodeRegion.start, vscodeRegion.end); 188 | } 189 | } 190 | 191 | function regionToVscodeRegion(region: ResultRegion): vscode.Range { 192 | return new vscode.Range(region.startLine - 1, region.startColumn - 1, region.endLine - 1, region.endColumn - 1); 193 | } 194 | -------------------------------------------------------------------------------- /src/extension/operations/openSarifFile.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | 4 | type FilePathAndContents = { 5 | filePath: string; 6 | fileContents: string; 7 | }; 8 | 9 | // Opens a SARIF file and return its contents 10 | export function openSarifFile(sarifFilePath: string): FilePathAndContents { 11 | // Validate that the file is a SARIF file 12 | if (!sarifFilePath.endsWith(".sarif")) { 13 | throw new Error("Not a SARIF file"); 14 | } 15 | 16 | // Check that the file exists 17 | if (!fs.existsSync(sarifFilePath)) { 18 | throw new Error("File does not exist"); 19 | } 20 | 21 | // Fetch the file contents 22 | const sarifFileContents = fs.readFileSync(sarifFilePath, "utf8"); 23 | return { 24 | filePath: sarifFilePath, 25 | fileContents: sarifFileContents, 26 | }; 27 | } 28 | 29 | export async function openSarifFileDialog(): Promise { 30 | const options: vscode.OpenDialogOptions = { 31 | defaultUri: vscode.workspace.workspaceFolders?.at(0)?.uri || undefined, 32 | canSelectMany: true, 33 | openLabel: "Open SARIF file", 34 | filters: { 35 | sarif: ["sarif"], 36 | }, 37 | }; 38 | 39 | const filePaths: string[] = []; 40 | await vscode.window.showOpenDialog(options).then((fileUris) => { 41 | for (const fileUri of fileUris || []) { 42 | filePaths.push(fileUri.fsPath); 43 | } 44 | }); 45 | 46 | return filePaths; 47 | } 48 | -------------------------------------------------------------------------------- /src/extension/weAuditInterface.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ExportedResult } from "../shared/resultTypes"; 3 | import { Entry, EntryType, FindingDifficulty, FindingSeverity, FindingType, Location } from "./weAuditTypes"; 4 | 5 | const weAuditExtensionId = "trailofbits.weaudit"; 6 | 7 | export class WeAuditNotInstalledError extends Error { 8 | constructor() { 9 | super("Please install the `weAudit` VSCode extension to use this feature"); 10 | this.name = "WeAuditNotInstalledError"; 11 | } 12 | 13 | public showInstallWeAuditVSCodeError(errorMsg: string) { 14 | vscode.window.showErrorMessage(`${errorMsg}: ${this.message}.`, "Install weAudit").then((selection) => { 15 | if (selection === "Install weAudit") { 16 | vscode.commands.executeCommand("workbench.extensions.action.showExtensionsWithIds", [ 17 | weAuditExtensionId, 18 | ]); 19 | } 20 | }); 21 | } 22 | } 23 | 24 | export function isWeAuditInstalled(): boolean { 25 | return vscode.extensions.getExtension(weAuditExtensionId) !== undefined; 26 | } 27 | 28 | export async function getGitHubPermalink(startLine: number, endLine: number, absolutePath: string): Promise { 29 | // Ensure that the weAudit extension is installed 30 | if (!isWeAuditInstalled()) { 31 | throw new WeAuditNotInstalledError(); 32 | } 33 | 34 | // Call the `weAudit.getClientPermalink` command of the weAudit extension 35 | // "weAudit.getClientPermalink", (location: Location): string 36 | const location: Location = { 37 | path: absolutePath, 38 | startLine: startLine, 39 | endLine: endLine, 40 | label: "", 41 | description: "", 42 | }; 43 | const permalink = (await vscode.commands.executeCommand("weAudit.getClientPermalink", location)) as string; 44 | return permalink; 45 | } 46 | 47 | async function exportedResultsToEntry(results: ExportedResult[]): Promise { 48 | // Ensure that every bug has the same ruleName 49 | if (!results.every((result) => result.rule.name === results[0].rule.name)) { 50 | throw new Error( 51 | "Failed to convert an ExportedResult list into a single weAudit Entry. Expected all items inside ExportedResult[] to be from the same rule.", 52 | ); 53 | } 54 | 55 | const rule = results[0].rule; 56 | 57 | const locations = []; 58 | for (const result of results) { 59 | const primaryLocation = result.locations[0]; 60 | const resultsHasDataflow = result.dataFlow.length > 0; 61 | 62 | // Add the result message and comment as location description 63 | let locationDescription = ""; 64 | locationDescription += result.message.trim(); 65 | if (result.note.comment !== "") { 66 | locationDescription += "\n\n"; 67 | locationDescription += result.note.comment.trim(); 68 | } 69 | 70 | // If we have dataFlow data, add it to the description of the location 71 | if (resultsHasDataflow) { 72 | locationDescription += "\n\n"; 73 | locationDescription += "Data Flow:\n"; 74 | for (let i = 0; i < result.dataFlow.length; i++) { 75 | const dataFlowElement = result.dataFlow[i]; 76 | const dataFlowLocation = dataFlowElement.location; 77 | 78 | let label; 79 | if (i === 0) { 80 | label = "Source"; 81 | } else if (i === result.dataFlow.length - 1) { 82 | label = "Sink"; 83 | } else { 84 | label = i.toString(); 85 | } 86 | locationDescription += " - " + label + ": "; 87 | locationDescription += await getGitHubPermalink( 88 | dataFlowLocation.region.startLine - 1, 89 | dataFlowLocation.region.endLine - 1, 90 | dataFlowLocation.path, 91 | ); 92 | locationDescription += " (" + dataFlowElement.message.trim() + ")\n"; 93 | } 94 | } 95 | 96 | // Add the primary location to the description. Add the primary location to the locations array 97 | // NOTE: We purposely do not add the dataflow locations to the locations array because 98 | // weAudit does not work well with overlapping locations which is often the case 99 | locations.push({ 100 | path: primaryLocation.path, 101 | startLine: primaryLocation.region.startLine - 1, 102 | endLine: primaryLocation.region.endLine - 1, 103 | label: "", 104 | description: locationDescription.trim(), 105 | }); 106 | } 107 | 108 | // Setup the recommendations 109 | let recommendations = rule.help; 110 | if (recommendations === "") { 111 | recommendations = rule.helpURI; 112 | } else if (rule.helpURI !== "") { 113 | recommendations += " ("; 114 | recommendations += rule.helpURI; 115 | recommendations += ")"; 116 | } 117 | 118 | const entry: Entry = { 119 | label: rule.name, 120 | entryType: EntryType.Finding, 121 | author: rule.toolName, 122 | locations: locations, 123 | details: { 124 | severity: FindingSeverity.Undefined, 125 | difficulty: FindingDifficulty.Undefined, 126 | type: FindingType.Undefined, 127 | description: rule.fullDescription.trim(), 128 | exploit: "", 129 | recommendation: recommendations.trim(), 130 | }, 131 | }; 132 | 133 | return entry; 134 | } 135 | 136 | export async function openGithubIssueFromResults(results: ExportedResult[]) { 137 | // Ensure that the weAudit extension is installed 138 | if (!isWeAuditInstalled()) { 139 | throw new WeAuditNotInstalledError(); 140 | } 141 | 142 | const entry = await exportedResultsToEntry(results); 143 | 144 | // "weAudit.openGithubIssue", (node: Entry) => { 145 | await vscode.commands.executeCommand("weAudit.openGithubIssue", entry); 146 | } 147 | 148 | export async function sendBugsToWeAudit(bugs: ExportedResult[]) { 149 | // Ensure that the weAudit extension is installed 150 | if (!isWeAuditInstalled()) { 151 | throw new WeAuditNotInstalledError(); 152 | } 153 | 154 | // Group bugs by rule 155 | const bugsByRule = new Map(); 156 | for (const bug of bugs) { 157 | const ruleName = bug.rule.name; 158 | if (!bugsByRule.has(ruleName)) { 159 | bugsByRule.set(ruleName, []); 160 | } 161 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 162 | bugsByRule.get(ruleName)!.push(bug); 163 | } 164 | 165 | const entries: Entry[] = []; 166 | for (const [_, bugs] of bugsByRule) { 167 | const entry = await exportedResultsToEntry(bugs); 168 | entries.push(entry); 169 | } 170 | 171 | // Call the `weAudit.externallyLoadFindings` command of the weAudit extension 172 | await vscode.commands.executeCommand("weAudit.externallyLoadFindings", entries); 173 | } 174 | -------------------------------------------------------------------------------- /src/extension/weAuditTypes.ts: -------------------------------------------------------------------------------- 1 | // ==================== 2 | // Github Issue Creation 3 | // create an enum with two values 4 | export enum FindingSeverity { 5 | Informational = "Informational", 6 | Undetermined = "Undetermined", 7 | Low = "Low", 8 | Medium = "Medium", 9 | High = "High", 10 | Undefined = "", 11 | } 12 | 13 | export enum FindingDifficulty { 14 | Undetermined = "Undetermined", 15 | NA = "N/A", 16 | Low = "Low", 17 | Medium = "Medium", 18 | High = "High", 19 | Undefined = "", 20 | } 21 | 22 | export enum FindingType { 23 | AccessControls = "Access Controls", 24 | AuditingAndLogging = "Auditing and Logging", 25 | Authentication = "Authentication", 26 | Configuration = "Configuration", 27 | Cryptography = "Cryptography", 28 | DataExposure = "Data Exposure", 29 | DataValidation = "Data Validation", 30 | DenialOfService = "Denial of Service", 31 | ErrorReporting = "Error Reporting", 32 | Patching = "Patching", 33 | SessionManagement = "Session Management", 34 | Testing = "Testing", 35 | Timing = "Timing", 36 | UndefinedBehavior = "Undefined Behavior", 37 | Undefined = "", 38 | } 39 | 40 | export enum EntryType { 41 | Finding, 42 | Note, 43 | PathOrganizer, 44 | } 45 | 46 | export interface EntryDetails { 47 | severity: FindingSeverity; 48 | difficulty: FindingDifficulty; 49 | type: FindingType; 50 | description: string; 51 | exploit: string; 52 | recommendation: string; 53 | } 54 | 55 | /** 56 | * A location in a file. 57 | */ 58 | export interface Location { 59 | /** The path relative to the base git directory */ 60 | path: string; 61 | 62 | /** The line where the entry starts */ 63 | startLine: number; 64 | 65 | /** The line where the entry ends */ 66 | endLine: number; 67 | 68 | /** The label of the location */ 69 | label: string; 70 | 71 | /** The description of the location. This is currently used only when externally loading entries */ 72 | description: string; 73 | } 74 | 75 | /** 76 | * Represents an entry in the finding tree. 77 | */ 78 | export interface Entry { 79 | /** The title of the entry */ 80 | label: string; 81 | 82 | /** The type of the entry (finding or note) */ 83 | entryType: EntryType; 84 | 85 | /** The author of the entry */ 86 | author: string; 87 | 88 | /** The details of the entry */ 89 | details: EntryDetails; 90 | 91 | /** Locations */ 92 | locations: Location[]; 93 | } 94 | -------------------------------------------------------------------------------- /src/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared code 2 | 3 | This folder contains code that is shared by the extension and the Webview. It includes: 4 | - Types shared by both codebases: [filter data types](./filterData.ts), [results types](./resultTypes.ts), and [webview message types](./webviewMessageTypes.ts) 5 | - Shared utility functions: [file related functions](./file.ts). 6 | -------------------------------------------------------------------------------- /src/shared/file.ts: -------------------------------------------------------------------------------- 1 | export function normalizePath(path: string, baseFolder: string): string { 2 | // remove "file://" from the beginning of the path 3 | if (path.startsWith("file://")) { 4 | path = path.substring("file://".length); 5 | } 6 | 7 | // if the file is NOT absolute and a base directory was provided, normalize it relative to the base directory 8 | if (!path.startsWith("/") && baseFolder !== "") { 9 | // remove "./" from the beginning of the path 10 | path = path.replace(/\.\//g, "/"); 11 | path = baseFolder + "/" + path; 12 | } 13 | 14 | path = path.replace(/\/\//g, "/"); 15 | return path; 16 | } 17 | 18 | export function getPathLeaf(path: string): string { 19 | const parts = path.split("/"); 20 | const len = parts.length; 21 | if (len === 0) { 22 | return ""; 23 | } else { 24 | return parts[len - 1]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/shared/filterData.ts: -------------------------------------------------------------------------------- 1 | export type FilterData = { 2 | keyword: string; 3 | includePaths: string[]; 4 | excludePaths: string[]; 5 | excludeRuleIds: string[]; 6 | excludeSarifFiles: string[]; 7 | 8 | includeLevelError: boolean; 9 | includeLevelWarning: boolean; 10 | includeLevelNote: boolean; 11 | includeLevelNone: boolean; 12 | 13 | includeStatusTodo: boolean; 14 | includeStatusFalsePositive: boolean; 15 | includeStatusBug: boolean; 16 | }; 17 | -------------------------------------------------------------------------------- /src/shared/resultTypes.ts: -------------------------------------------------------------------------------- 1 | export type Rule = { 2 | id: string; 3 | name: string; 4 | level: ResultLevel; 5 | 6 | shortDescription: string; 7 | fullDescription: string; 8 | help: string; 9 | helpURI: string; 10 | 11 | toolName: string; 12 | 13 | // Indicates if the rule is hidden in the UI 14 | isHidden: boolean; 15 | }; 16 | 17 | export enum ResultLevel { 18 | error, 19 | warning, 20 | note, 21 | none, 22 | 23 | default, // This represents a rule without a level. By default it should be handled as a warning 24 | } 25 | 26 | export type LabeledLocation = { 27 | label: string; 28 | location: ResultLocation; 29 | }; 30 | 31 | export type ResultLocation = { 32 | path: string; 33 | region: ResultRegion; 34 | }; 35 | 36 | export type ResultRegion = { 37 | startLine: number; 38 | startColumn: number; 39 | endLine: number; 40 | endColumn: number; 41 | }; 42 | 43 | /* eslint-disable @typescript-eslint/naming-convention */ 44 | export enum ResultStatus { 45 | Todo = 0, 46 | FalsePositive = 1, 47 | Bug = 2, 48 | } 49 | /* eslint-enable @typescript-eslint/naming-convention */ 50 | 51 | export type ResultNote = { 52 | comment: string; 53 | status: ResultStatus; 54 | }; 55 | 56 | export type ResultNotes = { 57 | [key: string]: ResultNote; 58 | }; 59 | 60 | export type DataFlowElement = { 61 | message: string; 62 | location: ResultLocation; 63 | }; 64 | 65 | export const DEFAULT_NOTES_COMMENT = ""; 66 | export const DEFAULT_NOTES_STATUS: ResultStatus = ResultStatus.Todo; 67 | 68 | // ==================== 69 | // The type we export from our Result class to be passed to the extension 70 | export type ExportedResult = { 71 | sarifPath: string; 72 | level: ResultLevel; 73 | rule: Rule; 74 | message: string; 75 | locations: ResultLocation[]; 76 | dataFlow: DataFlowElement[]; 77 | note: ResultNote; 78 | }; 79 | 80 | // ==================== 81 | // VSCode config types and defaults 82 | export type VSCodeConfig = { 83 | showFullPathInResultsTable: boolean; 84 | }; 85 | 86 | export function defaultVSCodeConfig(): VSCodeConfig { 87 | return { showFullPathInResultsTable: false }; 88 | } 89 | -------------------------------------------------------------------------------- /src/shared/webviewMessageTypes.ts: -------------------------------------------------------------------------------- 1 | import { FilterData } from "./filterData"; 2 | import { ResultRegion, ResultNote, ResultNotes, ResultLocation, ExportedResult, VSCodeConfig } from "./resultTypes"; 3 | 4 | export type ExtensionToWebviewMsgTypes = 5 | | OpenSarifFileResponse 6 | | SetSarifFileBaseFolder 7 | | WebviewIsReadyResponse 8 | | UpdateVSCodeConfig; 9 | 10 | export type WebviewToExtensionMsgTypes = 11 | | WebviewIsReady 12 | | LaunchOpenSarifFileDialog 13 | | OpenSarifFile 14 | | CloseSarifFile 15 | | SetResultsBaseFolder 16 | | FailedToParseSarifFile 17 | | OpenCodeRegion 18 | | SetResultNote 19 | | SetHiddenRule 20 | | SendBugsToWeAudit 21 | | CopyPermalink 22 | | OpenGitHubIssue 23 | | SetFilterData; 24 | 25 | // ==================== 26 | // ExtensionToWebviewMsgTypes 27 | // ==================== 28 | // Message with the results asked in OpenSarifFile 29 | export type OpenSarifFileResponse = { 30 | command: "openSarifFileResponse"; 31 | sarifFilePath: string; 32 | sarifFileContents: string; 33 | resultNotes: ResultNotes; 34 | hiddenRules: string[]; 35 | baseFolder: string; 36 | }; 37 | 38 | // Message use to update the resultsBaseFolder of an opened SARIF file 39 | export type SetSarifFileBaseFolder = { 40 | command: "setSarifFileBaseFolder"; 41 | sarifFilePath: string; 42 | resultsBaseFolder: string; 43 | }; 44 | 45 | // Initial start up information including the filter data 46 | export type WebviewIsReadyResponse = { 47 | command: "webviewIsReadyResponse"; 48 | filterData: FilterData; 49 | vscodeConfig: VSCodeConfig; 50 | }; 51 | 52 | // Update the VS Code config 53 | export type UpdateVSCodeConfig = { 54 | command: "updateVSCodeConfig"; 55 | vscodeConfig: VSCodeConfig; 56 | }; 57 | 58 | // ==================== 59 | // WebviewToExtensionMsgTypes 60 | // ==================== 61 | // Message noting that the webview is ready to receive messages 62 | export type WebviewIsReady = { 63 | command: "webviewIsReady"; 64 | }; 65 | 66 | // Set the filter data on the extension side 67 | export type SetFilterData = { 68 | command: "setFilterData"; 69 | filterData: FilterData; 70 | }; 71 | 72 | // Message asking the extension for the contents of a SARIF file. 73 | export type LaunchOpenSarifFileDialog = { 74 | command: "launchOpenSarifFileDialog"; 75 | }; 76 | 77 | // Message asking the extension for the contents of a SARIF file. 78 | export type OpenSarifFile = { 79 | command: "openSarifFile"; 80 | sarifFilePath: string; 81 | }; 82 | 83 | // Message noting that the webview failed to parse a SARIF file 84 | export type FailedToParseSarifFile = { 85 | command: "failedToParseSarifFile"; 86 | sarifFilePath: string; 87 | error: string; 88 | }; 89 | 90 | // Message noting that a SARIF file was closed 91 | export type CloseSarifFile = { 92 | command: "closeSarifFile"; 93 | sarifFilePath: string; 94 | }; 95 | 96 | export type SetResultsBaseFolder = { 97 | command: "setResultsBaseFolder"; 98 | sarifFilePath: string; 99 | resultsBaseFolder: string; 100 | }; 101 | 102 | // Message asking the extension to open a file at a specific region 103 | export type OpenCodeRegion = { 104 | command: "openCodeRegion"; 105 | sarifFilePath: string; 106 | resultFilePath: string; 107 | resultRegion: ResultRegion; 108 | 109 | resultsBaseFolder: string; 110 | }; 111 | 112 | // Message asking the extension to set the status and comment of a result 113 | export type SetResultNote = { 114 | command: "setResultNote"; 115 | sarifFilePath: string; 116 | resultId: string; 117 | note: ResultNote; 118 | }; 119 | 120 | // Set that a rule was hidden 121 | export type SetHiddenRule = { 122 | command: "setHiddenRule"; 123 | sarifFilePath: string; 124 | ruleId: string; 125 | isHidden: boolean; 126 | }; 127 | // Open the setting menu 128 | export type SendBugsToWeAudit = { 129 | command: "sendBugsToWeAudit"; 130 | bugs: ExportedResult[]; 131 | }; 132 | 133 | // Message to copy a permalink to the given path and lines 134 | export type CopyPermalink = { 135 | command: "copyPermalink"; 136 | sarifFilePath: string; 137 | location: ResultLocation; 138 | }; 139 | 140 | export type OpenGitHubIssue = { 141 | command: "openGitHubIssue"; 142 | bugs: ExportedResult[]; 143 | }; 144 | -------------------------------------------------------------------------------- /src/test/fakerule.yaml: -------------------------------------------------------------------------------- 1 | # write a semgrep rule to find calls to openCodeRegion 2 | 3 | rules: 4 | - id: open-code-region 5 | patterns: 6 | - pattern: ....openCodeRegion(...) 7 | message: openCodeRegion is deprecated 8 | languages: [typescript] 9 | severity: ERROR 10 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/sarif_files/circomspect.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "results": [ 5 | { 6 | "level": "warning", 7 | "locations": [], 8 | "message": { 9 | "text": "The file `/Users/user/docs/tob_repos/circomspect/test.circom` does not include a version pragma. Assuming version 2.0.8." 10 | }, 11 | "relatedLocations": [], 12 | "rule": { 13 | "id": "P1004" 14 | }, 15 | "ruleId": "P1004" 16 | }, 17 | { 18 | "level": "warning", 19 | "locations": [ 20 | { 21 | "id": 0, 22 | "message": { 23 | "text": "The assigned signal `out[k]` is not constrained here." 24 | }, 25 | "physicalLocation": { 26 | "artifactLocation": { 27 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 28 | }, 29 | "region": { 30 | "endColumn": 36, 31 | "endLine": 19, 32 | "startColumn": 11, 33 | "startLine": 19 34 | } 35 | } 36 | } 37 | ], 38 | "message": { 39 | "text": "Using the signal assignment operator `<--` does not constrain the assigned signal." 40 | }, 41 | "relatedLocations": [ 42 | { 43 | "id": 0, 44 | "message": { 45 | "text": "The signal `out[k]` is constrained here." 46 | }, 47 | "physicalLocation": { 48 | "artifactLocation": { 49 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 50 | }, 51 | "region": { 52 | "endColumn": 39, 53 | "endLine": 20, 54 | "startColumn": 11, 55 | "startLine": 20 56 | } 57 | } 58 | } 59 | ], 60 | "rule": { 61 | "id": "CS0005" 62 | }, 63 | "ruleId": "CS0005" 64 | }, 65 | { 66 | "level": "warning", 67 | "locations": [ 68 | { 69 | "id": 0, 70 | "message": { 71 | "text": "The value assigned to `e2` here does not influence witness or constraint generation." 72 | }, 73 | "physicalLocation": { 74 | "artifactLocation": { 75 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 76 | }, 77 | "region": { 78 | "endColumn": 13, 79 | "endLine": 17, 80 | "startColumn": 7, 81 | "startLine": 17 82 | } 83 | } 84 | } 85 | ], 86 | "message": { 87 | "text": "The value assigned to `e2` is not used in witness or constraint generation." 88 | }, 89 | "relatedLocations": [], 90 | "rule": { 91 | "id": "CS0008" 92 | }, 93 | "ruleId": "CS0008" 94 | }, 95 | { 96 | "level": "warning", 97 | "locations": [ 98 | { 99 | "id": 0, 100 | "message": { 101 | "text": "The value assigned to `lout` here does not influence witness or constraint generation." 102 | }, 103 | "physicalLocation": { 104 | "artifactLocation": { 105 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 106 | }, 107 | "region": { 108 | "endColumn": 30, 109 | "endLine": 22, 110 | "startColumn": 11, 111 | "startLine": 22 112 | } 113 | } 114 | } 115 | ], 116 | "message": { 117 | "text": "The value assigned to `lout` is not used in witness or constraint generation." 118 | }, 119 | "relatedLocations": [], 120 | "rule": { 121 | "id": "CS0008" 122 | }, 123 | "ruleId": "CS0008" 124 | }, 125 | { 126 | "level": "warning", 127 | "locations": [ 128 | { 129 | "id": 0, 130 | "message": { 131 | "text": "The value assigned to `lout` here does not influence witness or constraint generation." 132 | }, 133 | "physicalLocation": { 134 | "artifactLocation": { 135 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 136 | }, 137 | "region": { 138 | "endColumn": 19, 139 | "endLine": 4, 140 | "startColumn": 7, 141 | "startLine": 4 142 | } 143 | } 144 | } 145 | ], 146 | "message": { 147 | "text": "The value assigned to `lout` is not used in witness or constraint generation." 148 | }, 149 | "relatedLocations": [], 150 | "rule": { 151 | "id": "CS0008" 152 | }, 153 | "ruleId": "CS0008" 154 | }, 155 | { 156 | "level": "warning", 157 | "locations": [ 158 | { 159 | "id": 0, 160 | "message": { 161 | "text": "The value assigned to `e2` here does not influence witness or constraint generation." 162 | }, 163 | "physicalLocation": { 164 | "artifactLocation": { 165 | "uri": "file:///Users/user/docs/tob_repos/circomspect/test.circom" 166 | }, 167 | "region": { 168 | "endColumn": 23, 169 | "endLine": 23, 170 | "startColumn": 11, 171 | "startLine": 23 172 | } 173 | } 174 | } 175 | ], 176 | "message": { 177 | "text": "The value assigned to `e2` is not used in witness or constraint generation." 178 | }, 179 | "relatedLocations": [], 180 | "rule": { 181 | "id": "CS0008" 182 | }, 183 | "ruleId": "CS0008" 184 | } 185 | ], 186 | "tool": { 187 | "driver": { 188 | "downloadUri": "https://github.com/trailofbits/circomspect", 189 | "name": "Circomspect", 190 | "organization": "Trail of Bits", 191 | "rules": [ 192 | { 193 | "id": "P1004", 194 | "name": "no-compiler-version-warning" 195 | }, 196 | { 197 | "id": "CS0005", 198 | "name": "signal-assignment-statement" 199 | }, 200 | { 201 | "id": "CS0008", 202 | "name": "variable-without-side-effect" 203 | }, 204 | { 205 | "id": "CS0008", 206 | "name": "variable-without-side-effect" 207 | }, 208 | { 209 | "id": "CS0008", 210 | "name": "variable-without-side-effect" 211 | }, 212 | { 213 | "id": "CS0008", 214 | "name": "variable-without-side-effect" 215 | } 216 | ] 217 | } 218 | } 219 | } 220 | ], 221 | "version": "2.1.0" 222 | } 223 | -------------------------------------------------------------------------------- /src/test/sarif_files/clippy.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", 3 | "runs": [ 4 | { 5 | "results": [ 6 | { 7 | "level": "error", 8 | "locations": [ 9 | { 10 | "physicalLocation": { 11 | "artifactLocation": { 12 | "uri": "/Users/user/.rustup/toolchains/nightly-2022-11-21-aarch64-apple-darwin/lib/rustlib/src/rust/compiler/rustc_lint_defs/src/lib.rs" 13 | }, 14 | "region": { 15 | "byteLength": 17, 16 | "byteOffset": 26445, 17 | "endColumn": 59, 18 | "endLine": 750, 19 | "startColumn": 42, 20 | "startLine": 750 21 | } 22 | } 23 | } 24 | ], 25 | "message": { 26 | "text": "the name `SideEffectOnVec` is defined multiple times" 27 | }, 28 | "ruleId": "E0428", 29 | "ruleIndex": 0 30 | }, 31 | { 32 | "level": "error", 33 | "locations": [ 34 | { 35 | "physicalLocation": { 36 | "artifactLocation": { 37 | "uri": "src/lib.rs" 38 | }, 39 | "region": { 40 | "byteLength": 26, 41 | "byteOffset": 1556, 42 | "endColumn": 27, 43 | "endLine": 48, 44 | "startColumn": 1, 45 | "startLine": 48 46 | } 47 | } 48 | } 49 | ], 50 | "message": { 51 | "text": "the name `SideEffectOnVec` is defined multiple times" 52 | }, 53 | "ruleId": "E0428", 54 | "ruleIndex": 0 55 | }, 56 | { 57 | "level": "error", 58 | "locations": [ 59 | { 60 | "physicalLocation": { 61 | "artifactLocation": { 62 | "uri": "src/lib.rs" 63 | }, 64 | "region": { 65 | "byteLength": 4, 66 | "byteOffset": 267, 67 | "endColumn": 37, 68 | "endLine": 11, 69 | "startColumn": 33, 70 | "startLine": 11 71 | } 72 | } 73 | } 74 | ], 75 | "message": { 76 | "text": "unresolved import `clippy_utils::msrvs::Msrv`" 77 | }, 78 | "ruleId": "E0432", 79 | "ruleIndex": 1 80 | }, 81 | { 82 | "level": "error", 83 | "locations": [ 84 | { 85 | "physicalLocation": { 86 | "artifactLocation": { 87 | "uri": "/Users/user/.cargo/registry/src/github.com-1ecc6299db9ec823/dylint_linting-2.1.1/src/lib.rs" 88 | }, 89 | "region": { 90 | "byteLength": 15, 91 | "byteOffset": 2206, 92 | "endColumn": 28, 93 | "endLine": 75, 94 | "startColumn": 13, 95 | "startLine": 75 96 | } 97 | } 98 | } 99 | ], 100 | "message": { 101 | "text": "the trait bound `SideEffectOnVec: rustc_lint::LateLintPass<'_>` is not satisfied" 102 | }, 103 | "ruleId": "E0277", 104 | "ruleIndex": 2 105 | }, 106 | { 107 | "level": "warning", 108 | "locations": [ 109 | { 110 | "physicalLocation": { 111 | "artifactLocation": { 112 | "uri": "src/lib.rs" 113 | }, 114 | "region": { 115 | "byteLength": 23, 116 | "byteOffset": 59, 117 | "endColumn": 24, 118 | "endLine": 4, 119 | "startColumn": 1, 120 | "startLine": 4 121 | } 122 | } 123 | } 124 | ], 125 | "message": { 126 | "text": "unused extern crate" 127 | }, 128 | "ruleId": "unused_extern_crates", 129 | "ruleIndex": 3 130 | } 131 | ], 132 | "tool": { 133 | "driver": { 134 | "informationUri": "https://rust-lang.github.io/rust-clippy/", 135 | "name": "clippy", 136 | "rules": [ 137 | { 138 | "fullDescription": { 139 | "text": "`SideEffectOnVec` must be defined only once in the type namespace of this module\n" 140 | }, 141 | "id": "E0428" 142 | }, 143 | { 144 | "fullDescription": { 145 | "text": "" 146 | }, 147 | "id": "E0432" 148 | }, 149 | { 150 | "fullDescription": { 151 | "text": "the following other types implement trait `rustc_lint::LateLintPass<'tcx>`:\n >\n >\n >\n >\n >\n >\n >\n >\nand 39 others\nrequired for the cast from `SideEffectOnVec` to the object type `dyn rustc_lint::LateLintPass<'_> + rustc_data_structures::sync::Send`\n" 152 | }, 153 | "id": "E0277" 154 | }, 155 | { 156 | "fullDescription": { 157 | "text": "" 158 | }, 159 | "id": "unused_extern_crates" 160 | } 161 | ] 162 | } 163 | } 164 | } 165 | ], 166 | "version": "2.1.0" 167 | } -------------------------------------------------------------------------------- /src/test/sarif_files/fake.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "fake": "asd" 3 | } -------------------------------------------------------------------------------- /src/test/sarif_files/local.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json", 3 | "runs": [ 4 | { 5 | "invocations": [ 6 | { 7 | "executionSuccessful": true, 8 | "toolExecutionNotifications": [] 9 | } 10 | ], 11 | "results": [ 12 | { 13 | "fingerprints": { 14 | "matchBasedId/v1": "d929c0da098caacf26b02271f6c9acff7641351e723956cce52c67928de450aa98c5fef0974f2a43e6d0bef936e90f1ccf89cc987f8d3ada4e0824b337024067_0" 15 | }, 16 | "locations": [ 17 | { 18 | "physicalLocation": { 19 | "artifactLocation": { 20 | "uri": "src/extension/sarifViewerWebview.ts", 21 | "uriBaseId": "%SRCROOT%" 22 | }, 23 | "region": { 24 | "endColumn": 119, 25 | "endLine": 137, 26 | "snippet": { 27 | "text": " const newResultsBaseFolder = await openCodeRegion(msg.resultFilePath, msg.resultRegion, msg.resultsBaseFolder);" 28 | }, 29 | "startColumn": 9, 30 | "startLine": 137 31 | } 32 | } 33 | } 34 | ], 35 | "message": { 36 | "text": "openCodeRegion is deprecated" 37 | }, 38 | "ruleId": "src.test.open-code-region" 39 | }, 40 | { 41 | "fingerprints": { 42 | "matchBasedId/v1": "ab473d16c2c95190e2b0cd1278d4406bcc2ebe27a46eecde33a1978c0b09e5f76771646aa46b966d9d2b5b8678428674ee7f424eba048a2c592395bbf9754c6d_0" 43 | }, 44 | "locations": [ 45 | { 46 | "physicalLocation": { 47 | "artifactLocation": { 48 | "uri": "src/webviewSrc/result/resultsTableWidget.ts", 49 | "uriBaseId": "%SRCROOT%" 50 | }, 51 | "region": { 52 | "endColumn": 61, 53 | "endLine": 143, 54 | "snippet": { 55 | "text": " sarifFile.openCodeRegion(filePath, location.region);" 56 | }, 57 | "startColumn": 9, 58 | "startLine": 143 59 | } 60 | } 61 | } 62 | ], 63 | "message": { 64 | "text": "openCodeRegion is deprecated" 65 | }, 66 | "ruleId": "src.test.open-code-region" 67 | } 68 | ], 69 | "tool": { 70 | "driver": { 71 | "name": "semgrep", 72 | "rules": [ 73 | { 74 | "defaultConfiguration": { 75 | "level": "error" 76 | }, 77 | "fullDescription": { 78 | "text": "openCodeRegion is deprecated" 79 | }, 80 | "id": "src.test.open-code-region", 81 | "name": "src.test.open-code-region", 82 | "properties": { 83 | "precision": "very-high", 84 | "tags": [] 85 | }, 86 | "shortDescription": { 87 | "text": "openCodeRegion is deprecated" 88 | } 89 | } 90 | ], 91 | "semanticVersion": "1.0.0" 92 | } 93 | } 94 | } 95 | ], 96 | "version": "2.1.0" 97 | } 98 | -------------------------------------------------------------------------------- /src/test/sarif_files/multirun_test.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "timestamp": "2024-04-08T13:24:32Z" 4 | }, 5 | "version": "2.1.0", 6 | "$schema": "https://json.schemastore.org/sarif-2.1.0.json", 7 | "runs": [ 8 | { 9 | "tool": { 10 | "driver": { 11 | "name": "Tool1", 12 | "rules": [ 13 | { 14 | "id": "SQLInjection", 15 | "name": "User-Controlled SQL Injection", 16 | "shortDescription": { 17 | "text": "" 18 | }, 19 | "defaultConfiguration": { 20 | "level": "error" 21 | }, 22 | "properties": { 23 | "precision": "high", 24 | "security-severity": "7", 25 | "tags": [ 26 | "security" 27 | ] 28 | } 29 | } 30 | ], 31 | "version": "0.0.1" 32 | } 33 | }, 34 | "results": [ 35 | { 36 | "ruleId": "SQLInjection", 37 | "level": "error", 38 | "message": { 39 | "text": "User-Controlled Shell Injection" 40 | }, 41 | "locations": [ 42 | { 43 | "physicalLocation": { 44 | "artifactLocation": { 45 | "uri": "testproject/FindingFromTool2_shellInjection.java" 46 | }, 47 | "region": { 48 | "startLine": 50, 49 | "startColumn": 1, 50 | "endColumn": 2 51 | } 52 | } 53 | } 54 | ] 55 | }, 56 | { 57 | "ruleId": "SQLInjection", 58 | "level": "error", 59 | "message": { 60 | "text": "User-Controlled Shell Injection" 61 | }, 62 | "locations": [ 63 | { 64 | "physicalLocation": { 65 | "artifactLocation": { 66 | "uri": "testproject/FindingFromTool2_shellInjection.java" 67 | }, 68 | "region": { 69 | "startLine": 51, 70 | "startColumn": 1, 71 | "endColumn": 2 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | ] 78 | }, 79 | { 80 | "tool": { 81 | "driver": { 82 | "name": "Tool2", 83 | "rules": [ 84 | { 85 | "id": "ShellInjection", 86 | "name": "User-Controlled Shell Injection", 87 | "shortDescription": { 88 | "text": "" 89 | }, 90 | "defaultConfiguration": { 91 | "level": "error" 92 | }, 93 | "properties": { 94 | "precision": "high", 95 | "security-severity": "7", 96 | "tags": [ 97 | "security" 98 | ] 99 | } 100 | } 101 | ], 102 | "version": "0.0.1" 103 | } 104 | }, 105 | "results": [ 106 | { 107 | "ruleId": "ShellInjection", 108 | "level": "error", 109 | "message": { 110 | "text": "User-Controlled Shell Injection" 111 | }, 112 | "locations": [ 113 | { 114 | "physicalLocation": { 115 | "artifactLocation": { 116 | "uri": "testproject/FindingFromTool2_shellInjection.java" 117 | }, 118 | "region": { 119 | "startLine": 24, 120 | "startColumn": 1, 121 | "endColumn": 2 122 | } 123 | } 124 | } 125 | ] 126 | } 127 | ] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import * as glob from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/webviewSrc/README.md: -------------------------------------------------------------------------------- 1 | # Webview source code 2 | 3 | This folder contains the Webview source code. Some of its files and folders are: 4 | - The [main.ts](./main.ts) file that holds global state and communication with the extension. 5 | - The [main.html](./main.html) file that includes the HTML code which is the base of the Webview UI. Widgets files ([resultsTableWidget.ts](./result/resultsTableWidget.ts), [resultDetailsWidget.ts](./result/resultDetailsWidget.ts), [sarifFileListWidget.ts](./sarifFile/sarifFileListWidget.ts), and [sarifFileDetailsWidget.ts](./sarifFile/sarifFileDetailsWidget.ts)) use Javascript to further modify the HTML after loading. 6 | - The [style.scss](./style.scss) file that has the Webview's css code. 7 | - The [tabs.ts](./tabs.ts) file that manages switching between the results and SARIF files tab. 8 | - The [resizablePanels.ts](./resizablePanels/resizablePanels.ts) file that handles resizing the table and details panels in both tabs. 9 | - The [result](./result/) folder that includes all code that draws and holds state for the results tab. 10 | - The [sarifFile](./sarifFile/) folder includes all code that draws and holds state for the SARIF files tab. 11 | 12 | This code is compiled with webpack and outputted to `dist/webview.js` (configured in [webpack.config.js](../../webpack.config.js)). 13 | -------------------------------------------------------------------------------- /src/webviewSrc/extensionApi.ts: -------------------------------------------------------------------------------- 1 | import { ExportedResult, ResultLocation } from "../shared/resultTypes"; 2 | import { 3 | CloseSarifFile, 4 | CopyPermalink, 5 | OpenGitHubIssue, 6 | FailedToParseSarifFile, 7 | OpenSarifFile, 8 | SetResultsBaseFolder, 9 | OpenCodeRegion, 10 | SendBugsToWeAudit, 11 | SetHiddenRule, 12 | SetResultNote, 13 | WebviewIsReady, 14 | LaunchOpenSarifFileDialog, 15 | SetFilterData, 16 | } from "../shared/webviewMessageTypes"; 17 | import { Result } from "./result/result"; 18 | import { SarifFile } from "./sarifFile/sarifFile"; 19 | import { vscode } from "./vscode"; 20 | import { FilterData } from "../shared/filterData"; 21 | 22 | export function apiWebviewIsReady() { 23 | const msg: WebviewIsReady = { 24 | command: "webviewIsReady", 25 | }; 26 | vscode.postMessage(msg); 27 | } 28 | 29 | export function apiLaunchOpenSarifFileDialog() { 30 | // The main extension will handle opening the dialog and returning the file path to the webview 31 | const msg: LaunchOpenSarifFileDialog = { 32 | command: "launchOpenSarifFileDialog", 33 | }; 34 | vscode.postMessage(msg); 35 | 36 | // NOTE: The response handler will handle the response 37 | } 38 | 39 | export function apiOpenSarifFile(sarifFilePath: string) { 40 | const msg: OpenSarifFile = { 41 | command: "openSarifFile", 42 | sarifFilePath: sarifFilePath, 43 | }; 44 | vscode.postMessage(msg); 45 | 46 | // NOTE: The response handler will handle the response 47 | } 48 | 49 | export function apiFailedToParseSarifFile(sarifFilePath: string, error: string) { 50 | const msg: FailedToParseSarifFile = { 51 | command: "failedToParseSarifFile", 52 | sarifFilePath: sarifFilePath, 53 | error: error, 54 | }; 55 | vscode.postMessage(msg); 56 | } 57 | 58 | export function apiCloseSarifFile(sarifFilePath: string) { 59 | const msg: CloseSarifFile = { 60 | command: "closeSarifFile", 61 | sarifFilePath: sarifFilePath, 62 | }; 63 | vscode.postMessage(msg); 64 | } 65 | 66 | export function apiSetResultsBaseFolder(sarifFilePath: string, resultsBaseFolder: string) { 67 | const msg: SetResultsBaseFolder = { 68 | command: "setResultsBaseFolder", 69 | sarifFilePath: sarifFilePath, 70 | resultsBaseFolder: resultsBaseFolder, 71 | }; 72 | vscode.postMessage(msg); 73 | } 74 | 75 | export function apiOpenCodeRegion(sarifFile: SarifFile, location: ResultLocation) { 76 | const sarifFilePath = sarifFile.getSarifFilePath(); 77 | const resultsBaseFolder = sarifFile.getResultsBaseFolder(); 78 | 79 | const msg: OpenCodeRegion = { 80 | command: "openCodeRegion", 81 | sarifFilePath: sarifFilePath, 82 | resultsBaseFolder: resultsBaseFolder, 83 | 84 | resultFilePath: location.path, 85 | resultRegion: location.region, 86 | }; 87 | vscode.postMessage(msg); 88 | } 89 | 90 | export function apiSetResultNote(result: Result) { 91 | const msg: SetResultNote = { 92 | command: "setResultNote", 93 | sarifFilePath: result.getAssociatedSarifPath(), 94 | resultId: result.getResultId(), 95 | note: { 96 | status: result.getStatus(), 97 | comment: result.getComment(), 98 | }, 99 | }; 100 | vscode.postMessage(msg); 101 | } 102 | 103 | export function apiSetHiddenRule(sarifFilePath: string, ruleId: string, isHidden: boolean) { 104 | const msg: SetHiddenRule = { 105 | command: "setHiddenRule", 106 | sarifFilePath: sarifFilePath, 107 | ruleId: ruleId, 108 | isHidden: isHidden, 109 | }; 110 | vscode.postMessage(msg); 111 | } 112 | 113 | export function apiSendBugsToWeAudit(bugs: Result[]) { 114 | const msg: SendBugsToWeAudit = { 115 | command: "sendBugsToWeAudit", 116 | bugs: bugs.map((bug) => { 117 | return resultToExportedResult(bug); 118 | }), 119 | }; 120 | vscode.postMessage(msg); 121 | } 122 | 123 | export function apiCopyPermalink(result: Result) { 124 | const msg: CopyPermalink = { 125 | command: "copyPermalink", 126 | sarifFilePath: result.getAssociatedSarifPath(), 127 | location: result.getResultPrimaryLocation(), 128 | }; 129 | vscode.postMessage(msg); 130 | } 131 | 132 | export function apiSetFilterData(filterData: FilterData) { 133 | const msg: SetFilterData = { 134 | command: "setFilterData", 135 | filterData: filterData, 136 | }; 137 | vscode.postMessage(msg); 138 | } 139 | 140 | function resultToExportedResult(result: Result): ExportedResult { 141 | return { 142 | sarifPath: result.getAssociatedSarifPath(), 143 | level: result.getLevel(), 144 | rule: result.getRule(), 145 | message: result.getMessage(), 146 | locations: result.getLocations(), 147 | dataFlow: result.getDataFlow(), 148 | note: result.getNote(), 149 | } as ExportedResult; 150 | } 151 | 152 | export function apiExportGitHubIssue(bugs: Result[]) { 153 | const msg: OpenGitHubIssue = { 154 | command: "openGitHubIssue", 155 | bugs: bugs.map((bug) => { 156 | return resultToExportedResult(bug); 157 | }), 158 | }; 159 | vscode.postMessage(msg); 160 | } 161 | -------------------------------------------------------------------------------- /src/webviewSrc/main.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | Results 17 | SARIF files 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | Include Paths Containing: 33 | 34 | 35 | Exclude Paths Containing: 36 | 37 | 38 | 39 | Exclude Rules: 40 | 46 | 47 | 48 | Exclude SARIF Files: 49 | 55 | 56 | Level: 57 | error 58 | warning 59 | note 60 | none 61 | 62 | Status: 63 | Todo 64 | Bug 65 | False Positive 66 | 67 | 68 | 69 | 70 | 75 | 80 | 81 | 82 | 83 | 84 | 85 | Open SARIF files 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | File 107 | 108 | 109 | 110 | 111 | 112 | Message 113 | 114 | 115 | 116 | 117 | 118 | 119 | Line 120 | 121 | 122 | 123 | 124 | 125 | RuleID 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/webviewSrc/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtensionToWebviewMsgTypes, 3 | OpenSarifFileResponse, 4 | SetSarifFileBaseFolder, 5 | } from "../shared/webviewMessageTypes"; 6 | import { ResultsTableWidget } from "./result/resultsTableWidget"; 7 | import { SarifFileListWidget } from "./sarifFile/sarifFileListWidget"; 8 | import { TabManager } from "./tabs"; 9 | 10 | import "./style.scss"; 11 | import { initResizablePanels } from "./resizablePanels/resizablePanels"; 12 | import { SarifFile } from "./sarifFile/sarifFile"; 13 | import { apiFailedToParseSarifFile, apiWebviewIsReady } from "./extensionApi"; 14 | 15 | type State = { 16 | tabManager: TabManager; 17 | 18 | resultsTableWidget: ResultsTableWidget; 19 | sarifFileListWidget: SarifFileListWidget; 20 | }; 21 | 22 | let state: State; 23 | 24 | function init() { 25 | const _resultsTableWidget = new ResultsTableWidget(); 26 | state = { 27 | tabManager: new TabManager(), 28 | resultsTableWidget: _resultsTableWidget, 29 | sarifFileListWidget: new SarifFileListWidget(_resultsTableWidget), 30 | }; 31 | 32 | // Add an event listener to receive messages from the extension 33 | window.addEventListener("message", (event) => { 34 | const message: ExtensionToWebviewMsgTypes = event.data; 35 | handleWebviewMessage(message); 36 | }); 37 | 38 | // Tell the extension that the webview is ready 39 | apiWebviewIsReady(); 40 | 41 | initResizablePanels(); 42 | 43 | window.addEventListener("click", (event) => { 44 | state.resultsTableWidget.globalOnClick(event); 45 | }); 46 | } 47 | 48 | // Init everything when the DOM is ready 49 | document.addEventListener("DOMContentLoaded", function () { 50 | init(); 51 | }); 52 | 53 | function handleWebviewMessage(msg: ExtensionToWebviewMsgTypes) { 54 | // console.debug("[Webview] Received a '" + msg.command + "'"); 55 | // console.debug("[Webview] Received message from extension:", msg); 56 | 57 | switch (msg.command) { 58 | case "webviewIsReadyResponse": { 59 | state.resultsTableWidget.updateVSCodeConfig(msg.vscodeConfig); 60 | state.resultsTableWidget.resultsTable.setFilters(msg.filterData); 61 | state.resultsTableWidget.updateResultFiltersHTMLElements(); 62 | break; 63 | } 64 | case "openSarifFileResponse": { 65 | handleOpenSarifFileResponseMsg(msg); 66 | break; 67 | } 68 | case "setSarifFileBaseFolder": { 69 | handleSetSarifFileBaseFolderMsg(msg); 70 | break; 71 | } 72 | case "updateVSCodeConfig": { 73 | state.resultsTableWidget.updateVSCodeConfig(msg.vscodeConfig); 74 | break; 75 | } 76 | default: { 77 | console.error("[SARIF Explorer] Unknown message received from extension:", msg); 78 | } 79 | } 80 | } 81 | 82 | function handleOpenSarifFileResponseMsg(msg: OpenSarifFileResponse) { 83 | // If we already have the SARIF file open, reload it 84 | if (state.sarifFileListWidget.hasSarifFile(msg.sarifFilePath)) { 85 | state.sarifFileListWidget.removeSarifFileWithPath(msg.sarifFilePath); 86 | } 87 | 88 | // Parse the SARIF file contents into results 89 | try { 90 | const sarifFile = new SarifFile( 91 | msg.sarifFilePath, 92 | msg.sarifFileContents, 93 | msg.resultNotes, 94 | msg.hiddenRules, 95 | msg.baseFolder, 96 | ); 97 | state.sarifFileListWidget.addSarifFile(sarifFile); 98 | } catch (error) { 99 | apiFailedToParseSarifFile(msg.sarifFilePath, String(error)); 100 | return; 101 | } 102 | 103 | state.tabManager.showResultsTab(); 104 | } 105 | 106 | function handleSetSarifFileBaseFolderMsg(msg: SetSarifFileBaseFolder) { 107 | const sarifFile = state.sarifFileListWidget.getSarifFileListData().getSarifFile(msg.sarifFilePath); 108 | if (!sarifFile) { 109 | console.error( 110 | "[SARIF Explorer] handleSetSarifFileBaseFolderMsg: Could not find SARIF file for path:", 111 | msg.sarifFilePath, 112 | ); 113 | return; 114 | } 115 | 116 | if (sarifFile.getResultsBaseFolder() !== msg.resultsBaseFolder) { 117 | sarifFile.setResultsBaseFolder(msg.resultsBaseFolder); 118 | state.sarifFileListWidget 119 | .getSarifFileDetailsWidget() 120 | .updateBaseFolder(msg.sarifFilePath, msg.resultsBaseFolder); 121 | // Update the result's detailed view 122 | state.resultsTableWidget.render(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/webviewSrc/resizablePanels/resizablePanels.ts: -------------------------------------------------------------------------------- 1 | export function initResizablePanels() { 2 | const resizableDividers = document.getElementsByClassName("verticalResizableDivider"); 3 | for (let i = 0; i < resizableDividers.length; i++) { 4 | const it = resizableDividers[i]; 5 | initResizablePanel(it as HTMLElement); 6 | } 7 | } 8 | 9 | async function initResizablePanel(divider: HTMLElement) { 10 | // ==================== 11 | // Get all the required elements 12 | // ==================== 13 | // A divider should have one previous and one next sibling 14 | const prevSibling = divider.previousElementSibling as HTMLElement; 15 | const nextSibling = divider.nextElementSibling as HTMLElement; 16 | if (!prevSibling || !nextSibling) { 17 | console.error("[SARIF Explorer] No previous or next sibling found for resizable divider."); 18 | return; 19 | } 20 | 21 | const parent = divider.parentNode as HTMLElement; 22 | if (parent === null) { 23 | throw new Error("No parent found for resizable divider."); 24 | } 25 | 26 | // ==================== 27 | // Set the initial height of the panel to 70% of the parent's height 28 | // ==================== 29 | // The next panel should take up the remaining space 30 | nextSibling.style.flex = "1"; 31 | 32 | // Wait until the parent's height is not 0 (i.e., it is rendered) and then set the panel's height 33 | let parentHeight = 0; 34 | while (parentHeight === 0) { 35 | parentHeight = parent.getBoundingClientRect().height; 36 | await new Promise((resolve) => setTimeout(resolve, 10)); 37 | } 38 | // Set the panel's height to 70% of the parent's height in pixels (has to be in pixels for the overflow-y to work) 39 | const h = (parentHeight * 70) / 100; 40 | prevSibling.style.height = `${h}px`; 41 | 42 | // ==================== 43 | // Handle the dragging events 44 | // ==================== 45 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 46 | let x = 0; 47 | let y = 0; 48 | let prevSiblingHeight = 0; 49 | 50 | // Handle the mousedown event that's triggered when user drags the divider 51 | const mouseDownHandler = function (e: MouseEvent) { 52 | // Get the current mouse position 53 | x = e.clientX; 54 | y = e.clientY; 55 | prevSiblingHeight = prevSibling.getBoundingClientRect().height; 56 | 57 | // Attach the listeners to `document` 58 | document.addEventListener("mousemove", mouseMoveHandler); 59 | document.addEventListener("mouseup", mouseUpHandler); 60 | }; 61 | 62 | const mouseMoveHandler = function (e: MouseEvent) { 63 | // How far the mouse has been moved 64 | const dy = e.clientY - y; 65 | 66 | const parentHeight = parent.getBoundingClientRect().height; 67 | const h = ((prevSiblingHeight + dy) * 100) / parentHeight; 68 | prevSibling.style.height = `${h}%`; 69 | 70 | divider.style.cursor = "row-resize"; 71 | document.body.style.cursor = "row-resize"; 72 | 73 | // Prevent text selection 74 | prevSibling.style.userSelect = "none"; 75 | prevSibling.style.pointerEvents = "none"; 76 | nextSibling.style.userSelect = "none"; 77 | nextSibling.style.pointerEvents = "none"; 78 | }; 79 | 80 | const mouseUpHandler = function () { 81 | divider.style.removeProperty("cursor"); 82 | document.body.style.removeProperty("cursor"); 83 | 84 | prevSibling.style.removeProperty("user-select"); 85 | prevSibling.style.removeProperty("pointer-events"); 86 | nextSibling.style.removeProperty("user-select"); 87 | nextSibling.style.removeProperty("pointer-events"); 88 | 89 | // Remove the handlers of `mousemove` and `mouseup` 90 | document.removeEventListener("mousemove", mouseMoveHandler); 91 | document.removeEventListener("mouseup", mouseUpHandler); 92 | }; 93 | 94 | // Attach the handler 95 | divider.addEventListener("mousedown", mouseDownHandler); 96 | } 97 | -------------------------------------------------------------------------------- /src/webviewSrc/result/result.ts: -------------------------------------------------------------------------------- 1 | import { SarifFile, Tool } from "../sarifFile/sarifFile"; 2 | import { normalizePath } from "../../shared/file"; 3 | import { 4 | DataFlowElement, 5 | LabeledLocation, 6 | ResultLevel, 7 | ResultLocation, 8 | ResultNote, 9 | ResultStatus, 10 | Rule, 11 | } from "../../shared/resultTypes"; 12 | import { apiCopyPermalink, apiExportGitHubIssue, apiOpenCodeRegion, apiSetResultNote } from "../extensionApi"; 13 | 14 | export type ResultAndRow = { 15 | result: Result; 16 | row: HTMLTableRowElement; 17 | }; 18 | 19 | export class Result { 20 | private resultId: string; 21 | private sarifFile: SarifFile; 22 | private runIndex: number; 23 | private level: ResultLevel; 24 | private ruleId: string; 25 | private message: string; 26 | private locations: ResultLocation[]; 27 | private relatedLocations: Map; 28 | private dataFlow: DataFlowElement[] = []; 29 | 30 | private note: ResultNote; 31 | 32 | constructor( 33 | resultId: string, 34 | sarifFile: SarifFile, 35 | runIndex: number, 36 | level: ResultLevel, 37 | ruleId: string, 38 | message: string, 39 | locations: ResultLocation[], 40 | relatedLocations: Map, 41 | dataFlow: DataFlowElement[], 42 | status: ResultStatus, 43 | comment: string, 44 | ) { 45 | this.resultId = resultId; 46 | this.sarifFile = sarifFile; 47 | this.runIndex = runIndex; 48 | this.level = level; 49 | this.ruleId = ruleId; 50 | this.message = message; 51 | 52 | this.note = { 53 | status: status, 54 | comment: comment, 55 | }; 56 | 57 | // Default to empty region if location is missing. This is allowed by the SARIF spec 58 | if (locations.length === 0) { 59 | const fakeEmptyLocation: ResultLocation = { 60 | path: "", 61 | region: { startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 }, 62 | }; 63 | locations.push(fakeEmptyLocation); 64 | } 65 | 66 | this.locations = locations; 67 | this.relatedLocations = relatedLocations; 68 | this.dataFlow = dataFlow; 69 | } 70 | 71 | public getLevel(): ResultLevel { 72 | return this.level === ResultLevel.default ? ResultLevel.warning : this.level; 73 | } 74 | 75 | public getLevelStr(): string { 76 | return ResultLevel[this.getLevel()]; 77 | } 78 | 79 | public setLevel(level: ResultLevel) { 80 | return (this.level = level); 81 | } 82 | 83 | public getRule(): Rule { 84 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 85 | return this.sarifFile.getRunRule(this.ruleId, this.runIndex)!; 86 | } 87 | 88 | public getRuleId(): string { 89 | return this.ruleId; 90 | } 91 | 92 | public getMessage(): string { 93 | return this.message; 94 | } 95 | 96 | public getResultId(): string { 97 | return this.resultId; 98 | } 99 | 100 | public getResultIdWithSarifPath(): string { 101 | return this.getAssociatedSarifPath() + "|" + this.getResultId(); 102 | } 103 | 104 | public getNote(): ResultNote { 105 | return this.note; 106 | } 107 | 108 | public getStatus(): ResultStatus { 109 | return this.note.status; 110 | } 111 | 112 | public getStatusStr(): string { 113 | return ResultStatus[this.getStatus()]; 114 | } 115 | 116 | public setStatus(status: ResultStatus) { 117 | this.note.status = status; 118 | apiSetResultNote(this); 119 | } 120 | 121 | public hasComment(): boolean { 122 | return this.note.comment !== ""; 123 | } 124 | 125 | public getComment(): string { 126 | return this.note.comment; 127 | } 128 | 129 | public setComment(comment: string) { 130 | this.note.comment = comment; 131 | apiSetResultNote(this); 132 | } 133 | 134 | public getAssociatedSarifFile(): SarifFile { 135 | return this.sarifFile; 136 | } 137 | 138 | public getAssociatedRunIndex(): number { 139 | return this.runIndex; 140 | } 141 | 142 | public getAssociatedSarifPath(): string { 143 | return this.sarifFile.getSarifFilePath(); 144 | } 145 | 146 | // Result method to get the primary location 147 | public getResultPrimaryLocation(): ResultLocation { 148 | return this.locations[0]; 149 | } 150 | 151 | public getLocations(): ResultLocation[] { 152 | return this.locations; 153 | } 154 | 155 | // Result method to get the primary location 156 | public getLine(): number { 157 | return this.getResultPrimaryLocation().region.startLine; 158 | } 159 | 160 | public getTool(): Tool { 161 | return this.getAssociatedSarifFile().getRunTool(this.getAssociatedRunIndex()); 162 | } 163 | 164 | public getResultPath(): string { 165 | return this.getResultPrimaryLocation().path; 166 | } 167 | 168 | public getResultNormalizedPath(): string { 169 | return normalizePath(this.getResultPath(), this.sarifFile.getResultsBaseFolder()); 170 | } 171 | 172 | public getRelatedLocations(): Map { 173 | return this.relatedLocations; 174 | } 175 | 176 | public getDataFlow(): DataFlowElement[] { 177 | return this.dataFlow; 178 | } 179 | 180 | // ==================== 181 | public openPrimaryCodeRegion() { 182 | apiOpenCodeRegion(this.sarifFile, this.getResultPrimaryLocation()); 183 | } 184 | 185 | public openCodeRegion(location: ResultLocation) { 186 | apiOpenCodeRegion(this.sarifFile, location); 187 | } 188 | 189 | public openRelatedLocation(index: number) { 190 | const relatedLocation = this.relatedLocations.get(index); 191 | if (relatedLocation) { 192 | apiOpenCodeRegion(this.sarifFile, relatedLocation.location); 193 | } else { 194 | console.warn( 195 | "[SARIF Explorer] Could not find related location with index " + 196 | index + 197 | ". The message in the SARIF file is likely incorrect.", 198 | ); 199 | } 200 | } 201 | 202 | public exportAsGHIssue() { 203 | apiExportGitHubIssue([this]); 204 | } 205 | 206 | public copyPermalink() { 207 | apiCopyPermalink(this); 208 | } 209 | 210 | // ==================== 211 | // This function converts a message into HTML, creating clickable links 212 | public messageToHTML(msg: string, shouldRemoveNewLines: boolean): HTMLElement[] { 213 | const res: HTMLElement[] = []; 214 | 215 | if (msg === undefined) { 216 | const span = document.createElement("span"); 217 | span.innerText = "undefined"; 218 | res.push(span); 219 | return res; 220 | } 221 | 222 | // Regexes to split by 223 | const httpsLinkRegex = /https:\/\/[^\s/$.?#].[^\s]*/i; // https://example.com 224 | 225 | const relatedLocationLinkRegex = /\[[^\]]+\]\(\d+\)/; // [text](index) 226 | const relatedLocationLinkRegexWithGroups = /\[([^\]]+)\]\((\d+)\)/; 227 | 228 | const mdLinkRegex = new RegExp("\\[[^\\]]+\\]\\(" + httpsLinkRegex.source + "\\)"); // [text](https://example.com) 229 | const mdLinkRegexWithGroups = new RegExp("\\[([^\\]]+)\\]\\((" + httpsLinkRegex.source + ")\\)"); 230 | 231 | const combinedRegex = new RegExp( 232 | "(" + relatedLocationLinkRegex.source + "|" + mdLinkRegex.source + "|" + httpsLinkRegex.source + ")", 233 | "gmi", 234 | ); 235 | 236 | let messageParts: string[]; 237 | if (shouldRemoveNewLines) { 238 | const messageNoNewLines = msg.replace(/[\n]/gm, " "); 239 | messageParts = messageNoNewLines.split(combinedRegex); 240 | } else { 241 | messageParts = msg.split(combinedRegex); 242 | } 243 | 244 | for (let i = 0; i < messageParts.length; i++) { 245 | const part = messageParts[i]; 246 | 247 | let linkText = ""; 248 | let linkUrl = ""; 249 | let linkOnClick: ((e: MouseEvent) => void) | undefined = undefined; 250 | let justText = ""; 251 | let linkMatch; 252 | 253 | if ((linkMatch = part.match(relatedLocationLinkRegexWithGroups))) { 254 | linkText = linkMatch[1]; 255 | linkUrl = "#"; 256 | // This index is used to link to a file location defined elsewhere in the SARIF file 257 | const linkIndex = parseInt(linkMatch[2]); 258 | linkOnClick = (e) => { 259 | e.stopImmediatePropagation(); 260 | this.openRelatedLocation(linkIndex); 261 | }; 262 | } else if ((linkMatch = part.match(mdLinkRegexWithGroups))) { 263 | linkText = linkMatch[1]; 264 | linkUrl = linkMatch[2]; 265 | } else if ((linkMatch = part.match(httpsLinkRegex))) { 266 | const endPunctuationRegex = /[^\w\s/]+$/; 267 | const endPunctuationMatch = linkMatch[0].match(endPunctuationRegex); 268 | if (endPunctuationMatch) { 269 | justText = endPunctuationMatch[0]; 270 | } 271 | linkText = linkMatch[0].replace(endPunctuationRegex, ""); 272 | linkUrl = linkText; 273 | } else { 274 | justText = part; 275 | } 276 | 277 | // If we defined a link above, create a link element. Otherwise, create a span element 278 | if (linkText) { 279 | // Link 280 | const link = document.createElement("a"); 281 | link.href = linkUrl; 282 | link.innerText = linkText; 283 | if (linkOnClick) { 284 | link.onclick = linkOnClick; 285 | } 286 | 287 | res.push(link); 288 | } 289 | 290 | // Not a link 291 | if (justText) { 292 | const span = document.createElement("span"); 293 | span.innerText = justText; 294 | 295 | res.push(span); 296 | } 297 | } 298 | 299 | return res; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/webviewSrc/result/resultDetailsWidget.ts: -------------------------------------------------------------------------------- 1 | import { ResultAndRow } from "./result"; 2 | import { ResultsTableWidget } from "./resultsTableWidget"; 3 | import { getElementByIdOrThrow } from "../utils"; 4 | import { ResultLocation } from "../../shared/resultTypes"; 5 | 6 | export class ResultDetailsWidget { 7 | /* eslint-disable @typescript-eslint/naming-convention */ 8 | private RESULT_DETAILS_SUMMARY_DIV = "resultDetailsSummary"; 9 | private RESULT_DETAILS_TABLE_BODY = "resultDetailsTableBody"; 10 | private RESULT_DETAILS_BUTTONS = "resultDetailsButtons"; 11 | /* eslint-enable @typescript-eslint/naming-convention */ 12 | 13 | private resultsTableWidget: ResultsTableWidget; 14 | 15 | private detailsSummary: HTMLDivElement; 16 | private tableBody: HTMLTableElement; 17 | private buttons: HTMLDivElement; 18 | 19 | constructor(resultsTableWidget: ResultsTableWidget) { 20 | this.resultsTableWidget = resultsTableWidget; 21 | 22 | this.detailsSummary = getElementByIdOrThrow(this.RESULT_DETAILS_SUMMARY_DIV) as HTMLTableElement; 23 | this.tableBody = getElementByIdOrThrow(this.RESULT_DETAILS_TABLE_BODY) as HTMLTableElement; 24 | this.buttons = getElementByIdOrThrow(this.RESULT_DETAILS_BUTTONS) as HTMLDivElement; 25 | } 26 | 27 | // ==================== 28 | // Public functions 29 | // ==================== 30 | public getButtons(): HTMLDivElement { 31 | return this.buttons; 32 | } 33 | 34 | public clearDetails() { 35 | this.detailsSummary.innerText = "No result selected"; 36 | this.tableBody.innerText = ""; 37 | this.buttons.innerText = ""; 38 | this.buttons.classList.add("hidden"); 39 | } 40 | 41 | public updateDetails(resultAndRow: ResultAndRow) { 42 | const result = resultAndRow.result; 43 | 44 | // Summary 45 | this.detailsSummary.innerText = ""; 46 | 47 | // Buttons 48 | this.buttons.innerText = ""; 49 | const buttonsElement = this.resultsTableWidget.createResultButtons(resultAndRow); 50 | this.buttons.appendChild(buttonsElement); 51 | this.buttons.classList.remove("hidden"); 52 | 53 | // Table 54 | this.tableBody.innerText = ""; 55 | 56 | const appendRowToTable = ( 57 | key: string, 58 | value: string | HTMLElement, 59 | ): [HTMLTableCellElement, HTMLTableCellElement] => { 60 | const row = this.tableBody.insertRow(); 61 | const cellKey = row.insertCell(); 62 | const cellValue = row.insertCell(); 63 | cellKey.innerText = key; 64 | if (typeof value === "string") { 65 | cellValue.innerText = value; 66 | } else { 67 | cellValue.appendChild(value); 68 | } 69 | 70 | cellKey.classList.add("detailKey"); 71 | cellValue.classList.add("detailValue"); 72 | 73 | return [cellKey, cellValue]; 74 | }; 75 | 76 | const appendNavigationTableToTable = ( 77 | key: string, 78 | rows_data: { column1_text: string; column2_text: string; location: ResultLocation }[], 79 | ) => { 80 | // Check if any rows_data has a column1_text 81 | const hasColumn1Text = rows_data.some((row_data) => row_data.column1_text !== ""); 82 | 83 | // Create table with the same style as the main result's table 84 | const table = document.createElement("table"); 85 | table.classList.add("mainTable"); 86 | 87 | // Removes borders between cells 88 | table.setAttribute("rules", "none"); 89 | 90 | // This makes the table focusable and makes the keydown event works 91 | table.tabIndex = 0; 92 | 93 | const tableBody = table.createTBody(); 94 | 95 | for (let i = 0; i < rows_data.length; i++) { 96 | const row_data = rows_data[i]; 97 | 98 | // Create the row 99 | const row = tableBody.insertRow(); 100 | row.classList.add("detailTableRow"); 101 | 102 | if (hasColumn1Text) { 103 | // Create the text node 104 | const textCell = row.insertCell(); 105 | textCell.classList.add("detailTableKey"); 106 | 107 | const textNode = document.createTextNode(row_data.column1_text); 108 | textCell.appendChild(textNode); 109 | } 110 | 111 | // Create link to the code region 112 | const linkCell = row.insertCell(); 113 | const link = document.createElement("a"); 114 | link.href = "#"; 115 | link.innerText = row_data.column2_text; 116 | linkCell.appendChild(link); 117 | 118 | // Append the text node and link to the cell 119 | row.onclick = () => { 120 | result.openCodeRegion(row_data.location); 121 | }; 122 | 123 | // Set the onclick event for each row so that clicking ArrowDown and ArrowUp will work 124 | row.tabIndex = 0; 125 | row.addEventListener("keydown", (e: KeyboardEvent) => { 126 | if (e.key === "ArrowDown") { 127 | if (i < rows_data.length - 1) { 128 | const target = table.rows[i + 1]; 129 | target.focus(); 130 | target.click(); 131 | } 132 | } else if (e.key === "ArrowUp") { 133 | if (i > 0) { 134 | const target = table.rows[i - 1]; 135 | target.focus(); 136 | target.click(); 137 | } 138 | } 139 | }); 140 | } 141 | 142 | const div = document.createElement("div"); 143 | div.appendChild(table); 144 | const [_, cellValue] = appendRowToTable(key, div); 145 | cellValue.style.paddingTop = "0"; 146 | }; 147 | 148 | // Editable comment 149 | { 150 | const editableNodeTextArea = document.createElement("textarea"); 151 | editableNodeTextArea.placeholder = "Add a comment..."; 152 | editableNodeTextArea.value = result.getComment(); 153 | editableNodeTextArea.oninput = () => { 154 | result.setComment(editableNodeTextArea.value); 155 | this.resultsTableWidget.updateResultRowComment(resultAndRow); 156 | }; 157 | editableNodeTextArea.classList.add("detailEditableTextArea"); 158 | editableNodeTextArea.classList.add("inputArea"); 159 | 160 | appendRowToTable("Comment:", editableNodeTextArea); 161 | } 162 | 163 | // Rule 164 | { 165 | const rule = result.getRule(); 166 | const ruleDiv = document.createElement("div"); 167 | ruleDiv.classList.add("detailValueContainer"); 168 | 169 | const span0 = this.resultsTableWidget.createResultLevelIcon(result.getLevel()); 170 | 171 | const div0 = document.createElement("div"); 172 | const span1 = document.createElement("span"); 173 | span1.innerText = rule.name; 174 | 175 | const span2 = document.createElement("span"); 176 | span2.classList.add("secondaryText"); 177 | span2.innerText = " (" + rule.toolName + ")"; 178 | 179 | div0.appendChild(span1); 180 | div0.appendChild(span2); 181 | 182 | ruleDiv.appendChild(span0); 183 | ruleDiv.appendChild(div0); 184 | appendRowToTable("Rule:", ruleDiv); 185 | } 186 | 187 | // Rule description 188 | const rule = result.getRule(); 189 | const ruleDescription = rule.fullDescription || rule.shortDescription || ""; 190 | { 191 | if (ruleDescription && ruleDescription !== result.getMessage()) { 192 | const ruleDescriptionDiv = document.createElement("div"); 193 | for (const el of result.messageToHTML(ruleDescription, false)) { 194 | ruleDescriptionDiv.appendChild(el); 195 | } 196 | appendRowToTable("Description:", ruleDescriptionDiv); 197 | } 198 | } 199 | 200 | // Result message 201 | { 202 | const messageDiv = document.createElement("div"); 203 | for (const el of result.messageToHTML(result.getMessage(), false)) { 204 | messageDiv.appendChild(el); 205 | } 206 | appendRowToTable("Message:", messageDiv); 207 | } 208 | 209 | // Path 210 | { 211 | const pathElement = document.createElement("a"); 212 | pathElement.classList.add("wordBreakAll"); 213 | pathElement.href = result.getResultNormalizedPath(); 214 | pathElement.innerText = result.getResultNormalizedPath() + ":" + result.getLine().toString(); 215 | pathElement.onclick = () => { 216 | result.openPrimaryCodeRegion(); 217 | }; 218 | appendRowToTable("Path:", pathElement); 219 | } 220 | 221 | // Data Flow 222 | { 223 | const dataFlow = result.getDataFlow(); 224 | if (dataFlow.length > 0) { 225 | appendNavigationTableToTable( 226 | "Data Flow:", 227 | dataFlow.map((dataFlowElement, i) => { 228 | return { 229 | column1_text: 230 | i === 0 ? "Source: " : i === result.getDataFlow().length - 1 ? "Sink: " : `${i}: `, 231 | column2_text: dataFlowElement.message, 232 | location: dataFlowElement.location, 233 | }; 234 | }), 235 | ); 236 | } 237 | } 238 | 239 | // Rule help 240 | { 241 | let ruleHelp = rule.help; 242 | 243 | if (ruleHelp === ruleDescription || ruleHelp === result.getMessage()) { 244 | ruleHelp = ""; 245 | } 246 | 247 | if (ruleHelp === "") { 248 | ruleHelp = rule.helpURI; 249 | } else if (rule.helpURI !== "") { 250 | ruleHelp += " ("; 251 | ruleHelp += rule.helpURI; 252 | ruleHelp += ")"; 253 | } 254 | 255 | if (ruleHelp !== "") { 256 | const ruleHelpDiv = document.createElement("div"); 257 | for (const el of result.messageToHTML(ruleHelp, false)) { 258 | ruleHelpDiv.appendChild(el); 259 | } 260 | appendRowToTable("Help:", ruleHelpDiv); 261 | } 262 | } 263 | 264 | // Related Locations 265 | { 266 | const relatedLocations = result.getRelatedLocations(); 267 | if (relatedLocations.size > 0) { 268 | const relatedLocationsRows = []; 269 | for (const [_key, value] of relatedLocations.entries()) { 270 | relatedLocationsRows.push({ 271 | column1_text: "", // Empty so that the first column is suppressed 272 | column2_text: value.label 273 | ? value.label 274 | : value.location.path + ":" + value.location.region.startLine.toString(), 275 | location: value.location, 276 | }); 277 | } 278 | appendNavigationTableToTable("Related Locations:", relatedLocationsRows); 279 | } 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/webviewSrc/result/resultFilters.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "./result"; 2 | import { ResultLevel, ResultStatus } from "../../shared/resultTypes"; 3 | import { FilterData } from "../../shared/filterData"; 4 | import { apiSetFilterData } from "../extensionApi"; 5 | 6 | export class ResultsTableFilters { 7 | private d: FilterData = { 8 | keyword: "", 9 | includePaths: [], 10 | excludePaths: [], 11 | excludeRuleIds: [], 12 | excludeSarifFiles: [], 13 | 14 | includeLevelError: true, 15 | includeLevelWarning: true, 16 | includeLevelNote: true, 17 | includeLevelNone: true, 18 | 19 | includeStatusTodo: true, 20 | includeStatusFalsePositive: true, 21 | includeStatusBug: true, 22 | }; 23 | 24 | // Same information as above but as a set 25 | private includePathsAsSet: Set = new Set(); 26 | private excludePathsAsSet: Set = new Set(); 27 | private excludeRuleIdsAsSet: Set = new Set(); 28 | private excludeSarifFilesAsSet: Set = new Set(); 29 | 30 | // The main filtering function 31 | // Returns true if the result should be included in the table 32 | public filter(result: Result): boolean { 33 | return ( 34 | this.filterByRuleIds(result) && 35 | this.filterByKeyword(result) && 36 | this.filterByIncludePath(result) && 37 | this.filterByExcludePath(result) && 38 | this.filterBySarifFiles(result) && 39 | this.filterByLevel(result) && 40 | this.filterByStatus(result) 41 | ); 42 | } 43 | 44 | public filterByKeyword(result: Result): boolean { 45 | if (this.d.keyword === "") { 46 | return true; 47 | } 48 | 49 | // Checks are case insensitive 50 | const keyword = this.d.keyword.toLowerCase(); 51 | const rule = result.getRule(); 52 | return ( 53 | result.getResultPath().toLowerCase().includes(keyword) || 54 | result.getLine().toString().includes(keyword) || 55 | result.getMessage().toLowerCase().includes(keyword) || 56 | result.getTool().name.toLowerCase().includes(keyword) || 57 | result.getComment().toLowerCase().includes(keyword) || 58 | rule.id.toLowerCase().includes(keyword) || 59 | rule.name.toLowerCase().includes(keyword) || 60 | rule.shortDescription.toLowerCase().includes(keyword) || 61 | rule.fullDescription.toLowerCase().includes(keyword) || 62 | rule.help.toLowerCase().includes(keyword) || 63 | rule.helpURI.toLowerCase().includes(keyword) || 64 | rule.toolName.toLowerCase().includes(keyword) 65 | ); 66 | } 67 | 68 | public filterByIncludePath(result: Result): boolean { 69 | if (this.includePathsAsSet.size === 0) { 70 | return true; 71 | } 72 | 73 | const resPath = result.getResultNormalizedPath(); 74 | for (const p of this.includePathsAsSet) { 75 | if (resPath.includes(p)) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | } 81 | 82 | public filterByExcludePath(result: Result): boolean { 83 | if (this.excludePathsAsSet.size === 0) { 84 | return true; 85 | } 86 | 87 | const resPath = result.getResultNormalizedPath(); 88 | for (const p of this.excludePathsAsSet) { 89 | if (resPath.includes(p)) { 90 | return false; 91 | } 92 | } 93 | return true; 94 | } 95 | 96 | public filterByRuleIds(result: Result): boolean { 97 | if (this.excludeRuleIdsAsSet.size === 0) { 98 | return true; 99 | } 100 | 101 | const ruleId = result.getRuleId(); 102 | return !this.excludeRuleIdsAsSet.has(ruleId); 103 | } 104 | 105 | public filterBySarifFiles(result: Result): boolean { 106 | if (this.excludeSarifFilesAsSet.size === 0) { 107 | return true; 108 | } 109 | 110 | const sarifFilePath = result.getAssociatedSarifPath(); 111 | return !this.excludeSarifFilesAsSet.has(sarifFilePath); 112 | } 113 | 114 | public filterByLevel(result: Result): boolean { 115 | const level = result.getLevel(); 116 | if (this.d.includeLevelError && level === ResultLevel.error) { 117 | return true; 118 | } 119 | if (this.d.includeLevelWarning && level === ResultLevel.warning) { 120 | return true; 121 | } 122 | if (this.d.includeLevelNote && level === ResultLevel.note) { 123 | return true; 124 | } 125 | if (this.d.includeLevelNone && level === ResultLevel.none) { 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | public filterByStatus(result: Result): boolean { 132 | const status = result.getStatus(); 133 | if (this.d.includeStatusTodo && status === ResultStatus.Todo) { 134 | return true; 135 | } 136 | if (this.d.includeStatusBug && status === ResultStatus.Bug) { 137 | return true; 138 | } 139 | if (this.d.includeStatusFalsePositive && status === ResultStatus.FalsePositive) { 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | public getFilterData(): FilterData { 146 | return this.d; 147 | } 148 | 149 | public setKeyword(s: string) { 150 | this.d.keyword = s; 151 | apiSetFilterData(this.d); 152 | } 153 | 154 | public setIncludePaths(s: string) { 155 | this.includePathsAsSet = splitStringInParts(s); 156 | this.d.includePaths = Array.from(this.includePathsAsSet); 157 | apiSetFilterData(this.d); 158 | } 159 | 160 | public setExcludePaths(s: string) { 161 | this.excludePathsAsSet = splitStringInParts(s); 162 | this.d.excludePaths = Array.from(this.excludePathsAsSet); 163 | apiSetFilterData(this.d); 164 | } 165 | 166 | public setExcludedRuleIds(s: string) { 167 | this.excludeRuleIdsAsSet = splitStringInParts(s); 168 | this.d.excludeRuleIds = Array.from(this.excludeRuleIdsAsSet); 169 | apiSetFilterData(this.d); 170 | } 171 | 172 | public setExcludedSarifFiles(s: string) { 173 | this.excludeSarifFilesAsSet = splitStringInParts(s); 174 | this.d.excludeSarifFiles = Array.from(this.excludeSarifFilesAsSet); 175 | apiSetFilterData(this.d); 176 | } 177 | 178 | public setLevelError(b: boolean) { 179 | this.d.includeLevelError = b; 180 | apiSetFilterData(this.d); 181 | } 182 | 183 | public setLevelWarning(b: boolean) { 184 | this.d.includeLevelWarning = b; 185 | apiSetFilterData(this.d); 186 | } 187 | 188 | public setLevelNote(b: boolean) { 189 | this.d.includeLevelNote = b; 190 | apiSetFilterData(this.d); 191 | } 192 | 193 | public setLevelNone(b: boolean) { 194 | this.d.includeLevelNone = b; 195 | apiSetFilterData(this.d); 196 | } 197 | 198 | public setStatusTodo(b: boolean) { 199 | this.d.includeStatusTodo = b; 200 | apiSetFilterData(this.d); 201 | } 202 | 203 | public setStatusBug(b: boolean) { 204 | this.d.includeStatusBug = b; 205 | apiSetFilterData(this.d); 206 | } 207 | 208 | public setStatusFalsePositive(b: boolean) { 209 | this.d.includeStatusFalsePositive = b; 210 | apiSetFilterData(this.d); 211 | } 212 | 213 | public setFilters(filterData: FilterData) { 214 | this.d = filterData; 215 | this.includePathsAsSet = new Set(filterData.includePaths); 216 | this.excludePathsAsSet = new Set(filterData.excludePaths); 217 | this.excludeRuleIdsAsSet = new Set(filterData.excludeRuleIds); 218 | this.excludeSarifFilesAsSet = new Set(filterData.excludeSarifFiles); 219 | 220 | // Unnecessary to call `apiSetFilterData` here because this function is only called on initial setup 221 | } 222 | } 223 | 224 | export function splitStringInParts(s: string): Set { 225 | return new Set( 226 | s 227 | .split(",") 228 | .map((s) => s.trim()) 229 | .filter(Boolean), 230 | ); // Removes empty strings 231 | } 232 | 233 | export function setToStringInParts(s: Set): string { 234 | return Array.from(s).join(", "); 235 | } 236 | -------------------------------------------------------------------------------- /src/webviewSrc/result/resultsTable.ts: -------------------------------------------------------------------------------- 1 | import { FilterData } from "../../shared/filterData"; 2 | import { ResultStatus, Rule } from "../../shared/resultTypes"; 3 | import { Result } from "./result"; 4 | import { ResultsTableFilters } from "./resultFilters"; 5 | 6 | /* eslint-disable @typescript-eslint/naming-convention */ 7 | export enum SortDirection { 8 | Ascending = "asc", 9 | Descending = "desc", 10 | } 11 | 12 | export enum TableHeaders { 13 | FakeHeaderDropdownSymbol = 0, 14 | StatusSymbol = 1, 15 | File = 2, 16 | Message = 3, 17 | Line = 4, 18 | RuleID = 5, 19 | } 20 | /* eslint-enable @typescript-eslint/naming-convention */ 21 | 22 | type SortConfig = { 23 | // The unchanged header to sort by (e.g., the rule ID) 24 | unchangeableSortHeader: TableHeaders; 25 | // The main header to sort by 26 | mainHeader: TableHeaders; 27 | // The direction to sort by the main header 28 | mainDirection: SortDirection; 29 | // The secondary header to sort by 30 | secondaryHeader: TableHeaders; 31 | // The direction to sort by the secondary header 32 | secondaryDirection: SortDirection; 33 | }; 34 | 35 | export class ResultsTable { 36 | private results: Result[] = []; 37 | private sortConfig: SortConfig = { 38 | unchangeableSortHeader: TableHeaders.RuleID, 39 | 40 | mainHeader: TableHeaders.File, 41 | mainDirection: SortDirection.Ascending, 42 | secondaryHeader: TableHeaders.Message, 43 | secondaryDirection: SortDirection.Ascending, 44 | }; 45 | private filters: ResultsTableFilters = new ResultsTableFilters(); 46 | 47 | public getFilterData(): FilterData { 48 | return this.filters.getFilterData(); 49 | } 50 | 51 | // ==================== 52 | // results-related functions 53 | // ==================== 54 | // Returns the results in the table without the ones which are filtered 55 | public getFilteredResults(): Result[] { 56 | return this.results.filter((result) => this.filters.filter(result)); 57 | } 58 | 59 | public isResultFiltered(result: Result): boolean { 60 | return !this.filters.filter(result); 61 | } 62 | 63 | public isSarifFileFiltered(path: string): boolean { 64 | return this.filters.getFilterData().excludeSarifFiles.includes(path); 65 | } 66 | 67 | public getBugs(): Result[] { 68 | return this.results.filter((result) => result.getStatus() === ResultStatus.Bug); 69 | } 70 | 71 | // Add SARIF file to the list of open SARIF files 72 | public addResultsAndSort(results: Result[]) { 73 | this.results = this.results.concat(results); 74 | 75 | // After appending the results to the end, sort them 76 | this.sort(); 77 | } 78 | 79 | // Remove results based on their associated SARIF file path 80 | public removeResultsFromSarifPath(path: string) { 81 | this.results = this.results.filter((result) => result.getAssociatedSarifPath() !== path); 82 | } 83 | 84 | // Remove results based on their associated SARIF file path 85 | public isEmpty(): boolean { 86 | return this.results.length === 0; 87 | } 88 | 89 | // ==================== 90 | // Sort functions 91 | // ==================== 92 | public getSortConfig(): SortConfig { 93 | return this.sortConfig; 94 | } 95 | 96 | public setSortConfig(sortConfig: SortConfig) { 97 | this.sortConfig = sortConfig; 98 | } 99 | 100 | public setSortConfigAndSort(sortConfig: SortConfig) { 101 | this.setSortConfig(sortConfig); 102 | this.sort(); 103 | } 104 | 105 | public sortByHeader(headerToSortBy: TableHeaders) { 106 | if (this.sortConfig.mainHeader === headerToSortBy) { 107 | // If the column is already sorted, keep the same secondary header and reverse the sort direction 108 | this.setSortConfigAndSort({ 109 | unchangeableSortHeader: this.sortConfig.unchangeableSortHeader, 110 | 111 | mainHeader: headerToSortBy, 112 | mainDirection: 113 | this.sortConfig.mainDirection === SortDirection.Ascending 114 | ? SortDirection.Descending 115 | : SortDirection.Ascending, 116 | secondaryHeader: this.sortConfig.secondaryHeader, 117 | secondaryDirection: this.sortConfig.secondaryDirection, 118 | }); 119 | } else { 120 | // If the column is not sorted, sort it ascending 121 | this.setSortConfigAndSort({ 122 | unchangeableSortHeader: this.sortConfig.unchangeableSortHeader, 123 | 124 | mainHeader: headerToSortBy, 125 | mainDirection: SortDirection.Ascending, 126 | secondaryHeader: this.sortConfig.mainHeader, 127 | secondaryDirection: this.sortConfig.mainDirection, 128 | }); 129 | } 130 | } 131 | 132 | public sort() { 133 | const sc = this.sortConfig; 134 | 135 | // Our sorting function: it compares values and if they are equal, it keeps the original order 136 | const compareFunction = (r1: Result, r2: Result): number => { 137 | let res = this.compareResultsBy(r1, r2, sc.unchangeableSortHeader, SortDirection.Ascending); 138 | if (res === 0) { 139 | res = this.compareResultsBy(r1, r2, sc.mainHeader, sc.mainDirection); 140 | if (res === 0) { 141 | res = this.compareResultsBy(r1, r2, sc.secondaryHeader, sc.secondaryDirection); 142 | } 143 | } 144 | return res; 145 | }; 146 | 147 | // Sort the values 148 | this.results.sort(compareFunction); 149 | } 150 | 151 | private compareResultsBy(r1: Result, r2: Result, header: TableHeaders, direction: SortDirection): number { 152 | switch (header) { 153 | case TableHeaders.FakeHeaderDropdownSymbol: 154 | throw new Error("Cannot sort by the fake header"); 155 | case TableHeaders.StatusSymbol: 156 | if (r1.getStatus() < r2.getStatus()) { 157 | return direction === SortDirection.Ascending ? -1 : 1; 158 | } else if (r1.getStatus() > r2.getStatus()) { 159 | return direction === SortDirection.Ascending ? 1 : -1; 160 | } else { 161 | if (r1.hasComment() && !r2.hasComment()) { 162 | return direction === SortDirection.Ascending ? -1 : 1; 163 | } else if (!r1.hasComment() && r2.hasComment()) { 164 | return direction === SortDirection.Ascending ? 1 : -1; 165 | } else { 166 | return 0; 167 | } 168 | } 169 | case TableHeaders.File: 170 | if (r1.getResultPath() < r2.getResultPath()) { 171 | return direction === SortDirection.Ascending ? -1 : 1; 172 | } else if (r1.getResultPath() > r2.getResultPath()) { 173 | return direction === SortDirection.Ascending ? 1 : -1; 174 | } else if (r1.getLine() > r2.getLine()) { 175 | return 1; 176 | } else if (r1.getLine() < r2.getLine()) { 177 | return -1; 178 | } else { 179 | return 0; 180 | } 181 | case TableHeaders.Line: 182 | if (r1.getLine() < r2.getLine()) { 183 | return direction === SortDirection.Ascending ? -1 : 1; 184 | } else if (r1.getLine() > r2.getLine()) { 185 | return direction === SortDirection.Ascending ? 1 : -1; 186 | } else { 187 | return 0; 188 | } 189 | case TableHeaders.Message: 190 | if (r1.getMessage() < r2.getMessage()) { 191 | return direction === SortDirection.Ascending ? -1 : 1; 192 | } else if (r1.getMessage() > r2.getMessage()) { 193 | return direction === SortDirection.Ascending ? 1 : -1; 194 | } else { 195 | return 0; 196 | } 197 | case TableHeaders.RuleID: 198 | return this.compareRule(r1.getRule(), r2.getRule()); 199 | } 200 | } 201 | 202 | public compareRule(r1: Rule, r2: Rule): number { 203 | // Sort by level 204 | if (r1.level < r2.level) { 205 | return -1; 206 | } else if (r1.level > r2.level) { 207 | return 1; 208 | } else { 209 | // Sort by tool 210 | if (r1.toolName < r2.toolName) { 211 | return -1; 212 | } else if (r1.toolName > r2.toolName) { 213 | return 1; 214 | } else { 215 | // Sort alphabetically 216 | if (r1.id < r2.id) { 217 | return -1; 218 | } else if (r1.id > r2.id) { 219 | return 1; 220 | } else { 221 | return 0; 222 | } 223 | } 224 | } 225 | } 226 | 227 | // ==================== 228 | // Filter functions 229 | // ==================== 230 | public setKeywordFilter(s: string) { 231 | this.filters.setKeyword(s); 232 | } 233 | 234 | public setIncludePathFilter(s: string) { 235 | this.filters.setIncludePaths(s); 236 | } 237 | 238 | public setExcludePathFilter(s: string) { 239 | this.filters.setExcludePaths(s); 240 | } 241 | 242 | public setExcludedRuleIdFilter(s: string) { 243 | this.filters.setExcludedRuleIds(s); 244 | } 245 | 246 | public setExcludedSarifFilesFilter(s: string) { 247 | this.filters.setExcludedSarifFiles(s); 248 | } 249 | 250 | public setLevelErrorFilter(b: boolean) { 251 | this.filters.setLevelError(b); 252 | } 253 | 254 | public setLevelWarningFilter(b: boolean) { 255 | this.filters.setLevelWarning(b); 256 | } 257 | 258 | public setLevelNoteFilter(b: boolean) { 259 | this.filters.setLevelNote(b); 260 | } 261 | 262 | public setLevelNoneFilter(b: boolean) { 263 | this.filters.setLevelNone(b); 264 | } 265 | 266 | public setStatusTodoFilter(b: boolean) { 267 | this.filters.setStatusTodo(b); 268 | } 269 | 270 | public setStatusBugFilter(b: boolean) { 271 | this.filters.setStatusBug(b); 272 | } 273 | 274 | public setStatusFalsePositiveFilter(b: boolean) { 275 | this.filters.setStatusFalsePositive(b); 276 | } 277 | 278 | public setFilters(filterData: FilterData) { 279 | this.filters.setFilters(filterData); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/webviewSrc/sarifFile/sarifFileDetailsWidget.ts: -------------------------------------------------------------------------------- 1 | import { getElementByIdOrThrow } from "../utils"; 2 | import { SarifFileAndRow, SarifFileListWidget } from "./sarifFileListWidget"; 3 | 4 | export class SarifFileDetailsWidget { 5 | /* eslint-disable @typescript-eslint/naming-convention */ 6 | private SARIF_FILE_DETAILS_DIV = "sarifFileDetailsSummary"; 7 | private SARIF_FILE_DETAILS_TABLE_ID = "sarifFileDetailsTableBody"; 8 | private SARIF_FILE_DETAILS_BUTTONS = "sarifFileDetailsButtons"; 9 | /* eslint-enable @typescript-eslint/naming-convention */ 10 | 11 | private sarifFileWidget: SarifFileListWidget; 12 | 13 | private detailsSummary: HTMLDivElement; 14 | private tableBody: HTMLTableElement; 15 | private buttons: HTMLDivElement; 16 | 17 | private currentSarifFileAndRow: SarifFileAndRow | null = null; 18 | 19 | constructor(sarifFileWidget: SarifFileListWidget) { 20 | this.sarifFileWidget = sarifFileWidget; 21 | 22 | this.detailsSummary = getElementByIdOrThrow(this.SARIF_FILE_DETAILS_DIV) as HTMLDivElement; 23 | this.tableBody = getElementByIdOrThrow(this.SARIF_FILE_DETAILS_TABLE_ID) as HTMLTableElement; 24 | this.buttons = getElementByIdOrThrow(this.SARIF_FILE_DETAILS_BUTTONS) as HTMLDivElement; 25 | } 26 | 27 | public clearDetails() { 28 | this.detailsSummary.innerText = "No SARIF file selected"; 29 | this.tableBody.innerText = ""; 30 | this.buttons.innerText = ""; 31 | this.buttons.classList.add("hidden"); 32 | } 33 | 34 | public updateDetails(sarifFileAndRow: SarifFileAndRow) { 35 | this.currentSarifFileAndRow = sarifFileAndRow; 36 | const sarifFile = sarifFileAndRow.sarifFile; 37 | 38 | // Clear the summary 39 | this.detailsSummary.innerText = ""; 40 | 41 | // Buttons 42 | this.buttons.innerText = ""; 43 | const buttonsElement = this.sarifFileWidget.createSarifFileButtons(sarifFileAndRow); 44 | this.buttons.appendChild(buttonsElement); 45 | this.buttons.classList.remove("hidden"); 46 | 47 | // Add data of the result object to the details panel 48 | this.tableBody.innerText = ""; 49 | 50 | const appendRowToTable = (key: string, value: string | HTMLElement) => { 51 | const row = this.tableBody.insertRow(); 52 | const cellKey = row.insertCell(); 53 | const cellValue = row.insertCell(); 54 | cellKey.innerText = key; 55 | if (typeof value === "string") { 56 | cellValue.innerText = value; 57 | } else { 58 | cellValue.appendChild(value); 59 | } 60 | 61 | cellKey.classList.add("detailKey"); 62 | cellValue.classList.add("detailValue"); 63 | }; 64 | 65 | // Editable base folder 66 | { 67 | const editableBaseFolderNode = document.createElement("textarea"); 68 | editableBaseFolderNode.placeholder = 69 | "Add a base folder... (this is the folder from which your results' relative paths will be based one)"; 70 | editableBaseFolderNode.value = sarifFile.getResultsBaseFolder(); 71 | editableBaseFolderNode.oninput = () => { 72 | sarifFile.setResultsBaseFolder(editableBaseFolderNode.value); 73 | }; 74 | editableBaseFolderNode.classList.add("detailEditableTextArea"); 75 | editableBaseFolderNode.classList.add("inputArea"); 76 | editableBaseFolderNode.rows = 1; 77 | 78 | appendRowToTable("BaseFolder:", editableBaseFolderNode); 79 | } 80 | 81 | // Path node 82 | { 83 | const pathNode = document.createElement("span"); 84 | pathNode.innerText = sarifFile.getSarifFilePath(); 85 | pathNode.classList.add("wordBreakAll"); 86 | appendRowToTable("Path:", pathNode); 87 | } 88 | 89 | // Only display #Runs if there are more than 1 90 | if (sarifFile.getRunCount() > 1) { 91 | // #Runs node 92 | { 93 | appendRowToTable("#Runs:", sarifFile.getRunCount().toString()); 94 | } 95 | } 96 | 97 | for (let i = 0; i < sarifFile.getRunCount(); i++) { 98 | if (sarifFile.getRunCount() > 1) { 99 | // Separator 100 | const row = this.tableBody.insertRow(); 101 | row.style.borderBottom = "1px solid"; 102 | } 103 | // Tool with link to tool.informationUri (if it exists) 104 | const tool = sarifFile.getRunTool(i); 105 | { 106 | let toolElement: string | HTMLAnchorElement = tool.name; 107 | if (tool.informationUri !== "") { 108 | toolElement = document.createElement("a"); 109 | toolElement.classList.add("wordBreakAll"); 110 | toolElement.href = tool.informationUri; 111 | toolElement.innerText = tool.name; 112 | } 113 | appendRowToTable("Tool:", toolElement); 114 | } 115 | 116 | // Number of results 117 | { 118 | appendRowToTable("#Results:", sarifFile.getRunResults(i).length.toString()); 119 | } 120 | 121 | // Rules 122 | { 123 | const ruleIdsOrdered = Array.from(tool.rules.keys()).sort(); 124 | 125 | for (let i = 0; i < ruleIdsOrdered.length; i++) { 126 | const ruleId = ruleIdsOrdered[i]; 127 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 128 | const rule = tool.rules.get(ruleId)!; 129 | const ruleElement = document.createElement("li"); 130 | ruleElement.classList.add("wordBreakAll"); 131 | ruleElement.innerText = rule.name; 132 | 133 | if (i === 0) { 134 | appendRowToTable("Rules[]:", ruleElement); 135 | } else { 136 | appendRowToTable("", ruleElement); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | public updateBaseFolder(sarifFilePath: string, baseFolder: string) { 144 | if (this.currentSarifFileAndRow === null) { 145 | return; 146 | } 147 | 148 | if (this.currentSarifFileAndRow.sarifFile.getSarifFilePath() !== sarifFilePath) { 149 | return; 150 | } 151 | 152 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 153 | const editableBaseFolderNode = this.tableBody.querySelector("textarea")!; 154 | editableBaseFolderNode.value = baseFolder; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/webviewSrc/sarifFile/sarifFileList.ts: -------------------------------------------------------------------------------- 1 | import { SarifFile } from "./sarifFile"; 2 | 3 | export class SarifFileList { 4 | // Map of open SARIF files (key is the path, value is a SarifFile object) 5 | private openSarifFiles: Map = new Map(); 6 | 7 | // Returns true if the List contains the SARIF file 8 | public hasSarifFile(path: string) { 9 | return this.openSarifFiles.has(path); 10 | } 11 | 12 | // Returns true if the List contains the SARIF file 13 | public getSarifFile(path: string) { 14 | return this.openSarifFiles.get(path); 15 | } 16 | 17 | // Add SARIF file to the list of open SARIF files 18 | public addSarifFile(sarifFile: SarifFile) { 19 | const path = sarifFile.getSarifFilePath(); 20 | this.openSarifFiles.set(path, sarifFile); 21 | } 22 | 23 | // Remove SARIF file from the list of open SARIF files 24 | public removeSarifFile(path: string) { 25 | this.openSarifFiles.delete(path); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/webviewSrc/sarifFile/sarifFileListWidget.ts: -------------------------------------------------------------------------------- 1 | import { SarifFile } from "./sarifFile"; 2 | import { SarifFileList } from "./sarifFileList"; 3 | import { ResultsTableWidget } from "../result/resultsTableWidget"; 4 | import { SarifFileDetailsWidget } from "./sarifFileDetailsWidget"; 5 | import { getPathLeaf } from "../../shared/file"; 6 | import { apiCloseSarifFile, apiLaunchOpenSarifFileDialog, apiOpenSarifFile } from "../extensionApi"; 7 | import { setToStringInParts, splitStringInParts } from "../result/resultFilters"; 8 | import { getElementByIdOrThrow, scrollToRow } from "../utils"; 9 | 10 | export type SarifFileAndRow = { 11 | sarifFile: SarifFile; 12 | row: HTMLElement; 13 | }; 14 | 15 | class SelectedSarifFile { 16 | /* eslint-disable @typescript-eslint/naming-convention */ 17 | private SELECTED_ROW_CLASS = "selectedRow"; 18 | /* eslint-enable @typescript-eslint/naming-convention */ 19 | 20 | private sarifFileDetailsWidget: SarifFileDetailsWidget; 21 | 22 | // The currently selected result and HTML row 23 | private sarifFileAndRow: SarifFileAndRow | null = null; 24 | 25 | constructor(sarifFileDetailsWidget: SarifFileDetailsWidget) { 26 | this.sarifFileDetailsWidget = sarifFileDetailsWidget; 27 | 28 | // Clear the details panel 29 | this.sarifFileDetailsWidget.clearDetails(); 30 | } 31 | 32 | public getSarifFileAndRow(): SarifFileAndRow | null { 33 | if (this.sarifFileAndRow) { 34 | return this.sarifFileAndRow; 35 | } 36 | return null; 37 | } 38 | 39 | public getSarifFile(): SarifFile | null { 40 | if (this.sarifFileAndRow) { 41 | return this.sarifFileAndRow.sarifFile; 42 | } 43 | return null; 44 | } 45 | 46 | public getRow(): HTMLElement | null { 47 | if (this.sarifFileAndRow) { 48 | return this.sarifFileAndRow.row; 49 | } 50 | return null; 51 | } 52 | 53 | public clearSelection(): void { 54 | if (this.sarifFileAndRow === null) { 55 | return; 56 | } 57 | 58 | // Remove the selectedRow class from the previously selected row 59 | this.sarifFileAndRow.row.classList.remove(this.SELECTED_ROW_CLASS); 60 | 61 | this.sarifFileAndRow = null; 62 | 63 | // Clear the details panel 64 | this.sarifFileDetailsWidget.clearDetails(); 65 | } 66 | 67 | public setSarifFile(sarifFileAndRow: SarifFileAndRow): void { 68 | if (this.sarifFileAndRow !== null) { 69 | // Remove the selectedRow class from the previously selected row 70 | this.sarifFileAndRow.row.classList.remove(this.SELECTED_ROW_CLASS); 71 | } 72 | 73 | // Set the active class on the new row 74 | this.sarifFileAndRow = sarifFileAndRow; 75 | this.sarifFileAndRow.row.classList.add(this.SELECTED_ROW_CLASS); 76 | 77 | // Update the details widget accordingly 78 | this.sarifFileDetailsWidget.updateDetails(this.sarifFileAndRow); 79 | } 80 | } 81 | 82 | export class SarifFileListWidget { 83 | /* eslint-disable @typescript-eslint/naming-convention */ 84 | private SARIF_LIST_TABLE_ID = "sarifFileTable"; 85 | private SARIF_LIST_OPEN_BUTTON_ID = "openNewSarifFileButton"; 86 | private CLOSE_ALL_SARIF_FILES_BUTTON_ID = "closeAllSarifFilesButton"; 87 | /* eslint-enable @typescript-eslint/naming-convention */ 88 | 89 | private sarifFileListData: SarifFileList; 90 | 91 | private selectedSarifFile: SelectedSarifFile; 92 | 93 | private sarifFileTableElement: HTMLTableElement; 94 | 95 | private resultTableWidget: ResultsTableWidget; 96 | private sarifFileDetailsWidget: SarifFileDetailsWidget; 97 | 98 | private sarifFilePathToRow: Map; 99 | 100 | constructor(resultTableWidget: ResultsTableWidget) { 101 | this.resultTableWidget = resultTableWidget; 102 | this.sarifFileListData = new SarifFileList(); 103 | this.sarifFileDetailsWidget = new SarifFileDetailsWidget(this); 104 | this.selectedSarifFile = new SelectedSarifFile(this.sarifFileDetailsWidget); 105 | this.sarifFilePathToRow = new Map(); 106 | 107 | // Get the 'sarifFileList' element from the DOM and store in the class 108 | this.sarifFileTableElement = getElementByIdOrThrow(this.SARIF_LIST_TABLE_ID) as HTMLTableElement; 109 | 110 | // Add a click handler to the open SARIF file button 111 | const openNewSarifFileButton = getElementByIdOrThrow(this.SARIF_LIST_OPEN_BUTTON_ID) as HTMLButtonElement; 112 | openNewSarifFileButton.onclick = () => { 113 | apiLaunchOpenSarifFileDialog(); 114 | }; 115 | 116 | // Close all SARIF files button 117 | const closeAllSarifFilesButton = getElementByIdOrThrow( 118 | this.CLOSE_ALL_SARIF_FILES_BUTTON_ID, 119 | ) as HTMLButtonElement; 120 | closeAllSarifFilesButton.onclick = () => { 121 | this.sarifFilePathToRow.forEach((sarifFileAndRow) => { 122 | this.removeSarifFile(sarifFileAndRow); 123 | apiCloseSarifFile(sarifFileAndRow.sarifFile.getSarifFilePath()); 124 | }); 125 | }; 126 | 127 | this.sarifFileTableElement.setAttribute("tabindex", "0"); 128 | 129 | this.sarifFileTableElement.addEventListener("keydown", (e: KeyboardEvent) => { 130 | const selectedRow = this.selectedSarifFile.getRow(); 131 | 132 | switch (e.code) { 133 | case "ArrowDown": 134 | if (selectedRow) { 135 | this.setSelectedResultBelow(selectedRow); 136 | } 137 | e.preventDefault(); 138 | break; 139 | case "ArrowUp": 140 | if (selectedRow) { 141 | this.setSelectedResultAbove(selectedRow); 142 | } 143 | e.preventDefault(); 144 | break; 145 | } 146 | }); 147 | } 148 | 149 | // ==================== 150 | // Public functions 151 | // ==================== 152 | public getSarifFileDetailsWidget(): SarifFileDetailsWidget { 153 | return this.sarifFileDetailsWidget; 154 | } 155 | 156 | public addSarifFile(sarifFile: SarifFile) { 157 | this.sarifFileListData.addSarifFile(sarifFile); 158 | this.resultTableWidget.addResults(sarifFile); 159 | 160 | // Add the row to the table 161 | const row = this.sarifFileTableElement.insertRow(); 162 | row.title = sarifFile.getSarifFilePath(); 163 | 164 | // Add a fake cell to make style consistent with the results table. This makes sure the padding is the same 165 | const cell1 = row.insertCell(); 166 | cell1.classList.add("fakeCell"); 167 | cell1.innerText = "A"; 168 | 169 | // Add the path that, if clicked, will show the details in the details panel 170 | const cell2 = row.insertCell(); 171 | cell2.classList.add("cellWithButtons"); 172 | 173 | const pathContent = document.createElement("div"); 174 | pathContent.classList.add("cellWithButtonsContent"); 175 | pathContent.textContent = getPathLeaf(sarifFile.getSarifFilePath()) + " "; 176 | 177 | // Create the secondaryText with the full path 178 | const fullPathSpan = document.createElement("span"); 179 | fullPathSpan.classList.add("secondaryText"); 180 | fullPathSpan.textContent = sarifFile.getSarifFilePath(); 181 | pathContent.appendChild(fullPathSpan); 182 | 183 | row.onclick = () => { 184 | if (!this.sarifFileListData.hasSarifFile(sarifFile.getSarifFilePath())) { 185 | return; 186 | } 187 | 188 | this.selectedSarifFile.setSarifFile({ 189 | row: row, 190 | sarifFile: sarifFile, 191 | }); 192 | }; 193 | 194 | const sarifFileRow: SarifFileAndRow = { sarifFile, row }; 195 | const rowButtons = this.createSarifFileButtons(sarifFileRow); 196 | this.sarifFilePathToRow.set(sarifFile.getSarifFilePath(), sarifFileRow); 197 | cell2.appendChild(pathContent); 198 | cell2.appendChild(rowButtons); 199 | } 200 | 201 | public createSarifFileButtons(sarifFileAndRow: SarifFileAndRow): HTMLDivElement { 202 | const sarifFile = sarifFileAndRow.sarifFile; 203 | const row = sarifFileAndRow.row; 204 | 205 | const rowButtons = document.createElement("div"); 206 | rowButtons.classList.add("rowButtons"); 207 | 208 | // Close button 209 | const closeButton = document.createElement("div"); 210 | closeButton.classList.add("rowButton"); 211 | closeButton.classList.add("codicon"); 212 | closeButton.classList.add("codicon-close"); 213 | closeButton.onclick = (e) => { 214 | e.stopPropagation(); 215 | this.removeSarifFile(sarifFileAndRow); 216 | apiCloseSarifFile(sarifFileAndRow.sarifFile.getSarifFilePath()); 217 | }; 218 | closeButton.title = "Close this SARIF file"; 219 | 220 | // Hide button 221 | const hideButton = document.createElement("div"); 222 | hideButton.classList.add("rowButton"); 223 | hideButton.classList.add("codicon"); 224 | // If the result is filtered out, show the "eye closed" icon 225 | if (this.isSarifFileFiltered(sarifFile.getSarifFilePath())) { 226 | hideButton.classList.add("codicon-eye-closed"); 227 | row.classList.add(this.resultTableWidget.RULE_ROW_FILTERED_OUT_CLASS); 228 | } else { 229 | hideButton.classList.add("codicon-eye"); 230 | } 231 | hideButton.onclick = (e) => { 232 | e.stopPropagation(); 233 | 234 | // Add this ruleID to the FILTER_RULE_ID filter 235 | const filterSarifFilesElement = getElementByIdOrThrow( 236 | this.resultTableWidget.FILTER_SARIF_FILES_ID, 237 | ) as HTMLTextAreaElement; 238 | const excludedSarifFiles = splitStringInParts(filterSarifFilesElement.value); 239 | if (excludedSarifFiles.has(sarifFile.getSarifFilePath())) { 240 | excludedSarifFiles.delete(sarifFile.getSarifFilePath()); 241 | // Update the "hide" button with the correct icon 242 | hideButton.classList.remove("codicon-eye-closed"); 243 | hideButton.classList.add("codicon-eye"); 244 | row.classList.remove(this.resultTableWidget.RULE_ROW_FILTERED_OUT_CLASS); 245 | } else { 246 | excludedSarifFiles.add(sarifFile.getSarifFilePath()); 247 | // Update the "hide" button with the correct icon 248 | hideButton.classList.remove("codicon-eye"); 249 | hideButton.classList.add("codicon-eye-closed"); 250 | row.classList.add(this.resultTableWidget.RULE_ROW_FILTERED_OUT_CLASS); 251 | } 252 | 253 | filterSarifFilesElement.value = setToStringInParts(excludedSarifFiles); 254 | filterSarifFilesElement.dispatchEvent(new Event("input")); 255 | }; 256 | hideButton.title = "Hide/Show this SARIF file's results from the results table"; 257 | 258 | // Refresh button 259 | const refreshButton = document.createElement("div"); 260 | refreshButton.classList.add("rowButton"); 261 | refreshButton.classList.add("codicon"); 262 | refreshButton.classList.add("codicon-refresh"); 263 | refreshButton.onclick = (e) => { 264 | e.stopPropagation(); 265 | 266 | // Remove and reopen the SARIF file 267 | const sarifFilePath = sarifFile.getSarifFilePath(); 268 | this.removeSarifFile(sarifFileAndRow); 269 | apiOpenSarifFile(sarifFilePath); 270 | }; 271 | refreshButton.title = "Reload results for this SARIF file"; 272 | 273 | rowButtons.appendChild(refreshButton); 274 | rowButtons.appendChild(hideButton); 275 | rowButtons.appendChild(closeButton); 276 | 277 | return rowButtons; 278 | } 279 | 280 | public removeSarifFile(sarifFileAndRow: SarifFileAndRow) { 281 | const sarifFile = sarifFileAndRow.sarifFile; 282 | 283 | // If the selected row is the one we're removing, clear the selection 284 | const selectSarifFileOrNull = this.selectedSarifFile.getSarifFile(); 285 | if (selectSarifFileOrNull && selectSarifFileOrNull.getSarifFilePath() === sarifFile.getSarifFilePath()) { 286 | this.selectedSarifFile.clearSelection(); 287 | } 288 | 289 | // Remove the row from the table 290 | sarifFileAndRow.row.remove(); 291 | 292 | // Remove the SARIF file from the list and results table 293 | this.sarifFileListData.removeSarifFile(sarifFile.getSarifFilePath()); 294 | this.resultTableWidget.removeResults(sarifFile); 295 | // Remove the SARIF file path from the map 296 | this.sarifFilePathToRow.delete(sarifFile.getSarifFilePath()); 297 | } 298 | 299 | public removeSarifFileWithPath(sarifFilePath: string) { 300 | const row = this.sarifFilePathToRow.get(sarifFilePath); 301 | if (!row) { 302 | return; 303 | } 304 | this.removeSarifFile(row); 305 | } 306 | 307 | public hasSarifFile(sarifFilePath: string): boolean { 308 | return this.sarifFileListData.hasSarifFile(sarifFilePath); 309 | } 310 | 311 | public getSarifFileListData(): SarifFileList { 312 | return this.sarifFileListData; 313 | } 314 | 315 | // ==================== 316 | private isSarifFileFiltered(sarifFilePath: string): boolean { 317 | return this.resultTableWidget.resultsTable.isSarifFileFiltered(sarifFilePath); 318 | } 319 | 320 | // ==================== 321 | // For keyboard shortcuts 322 | private setSelectedResultBelow(selectedRow: HTMLElement) { 323 | const nextRow = selectedRow.nextElementSibling as HTMLElement; 324 | if (!nextRow) { 325 | return; 326 | } 327 | 328 | scrollToRow(nextRow); 329 | nextRow.click(); 330 | } 331 | 332 | private setSelectedResultAbove(selectedRow: HTMLElement) { 333 | const prevRow = selectedRow.previousElementSibling as HTMLElement; 334 | if (!prevRow) { 335 | return; 336 | } 337 | 338 | scrollToRow(prevRow); 339 | prevRow.click(); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/webviewSrc/style.scss: -------------------------------------------------------------------------------- 1 | @import url("../../node_modules/@vscode/codicons/dist/codicon.css"); 2 | 3 | // top | right | bottom | left 4 | $panel-padding: 5px 5px 10px 10px; 5 | 6 | html { 7 | height: 100%; 8 | padding: 0; 9 | margin: 0; 10 | overflow: auto; 11 | } 12 | 13 | body { 14 | padding: 0; 15 | height: 100%; 16 | } 17 | 18 | textarea { 19 | color: inherit; 20 | font-family: inherit; 21 | font-weight: inherit; 22 | font-size: inherit; 23 | } 24 | 25 | .hidden { 26 | display: none !important; 27 | } 28 | 29 | // Do not replace this with just ".hidden". It will fail to properly show the button when no files are opened 30 | .hidden_tab { 31 | display: none !important; 32 | } 33 | 34 | .invisible { 35 | visibility: hidden !important; 36 | } 37 | 38 | .ellipsis { 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | overflow: hidden; 42 | } 43 | 44 | .ellipsis-beginning { 45 | text-overflow: ellipsis; 46 | white-space: nowrap; 47 | overflow: hidden; 48 | direction: rtl; 49 | } 50 | 51 | .wordBreakAll { 52 | word-break: break-all; 53 | } 54 | 55 | .smallButton { 56 | padding: 2px; 57 | } 58 | 59 | .smallButton:hover { 60 | background-color: var(--vscode-toolbar-hoverBackground); 61 | cursor: pointer; 62 | } 63 | 64 | .panel { 65 | padding: $panel-padding; 66 | overflow-y: scroll; 67 | } 68 | 69 | .secondaryText { 70 | color: var(--vscode-descriptionForeground); 71 | } 72 | 73 | .countBadge { 74 | padding: 3px 6px; 75 | border-radius: 11px; 76 | font-size: 11px; 77 | // min-width: 18px; 78 | // min-height: 18px; 79 | line-height: 11px; 80 | font-weight: 400; 81 | text-align: center; 82 | background-color: var(--vscode-badge-background); 83 | color: var(--vscode-badge-foreground); 84 | border: 1px solid var(--vscode-contrastBorder); 85 | } 86 | 87 | .rowButton { 88 | padding: 2px; 89 | border-radius: 1px; 90 | } 91 | 92 | .rowButton:hover { 93 | background-color: var(--vscode-inputOption-hoverBackground); 94 | cursor: pointer; 95 | } 96 | 97 | .rowButtonDivider { 98 | border-left: 1px solid var(--vscode-foreground); 99 | margin-left: 3px; 100 | margin-right: 3px; 101 | } 102 | 103 | .warningIcon { 104 | color: var(--vscode-editorWarning-foreground); 105 | } 106 | 107 | .errorIcon { 108 | color: var(--vscode-editorError-foreground); 109 | } 110 | 111 | .noteIcon { 112 | color: var(--vscode-editorInfo-foreground); 113 | } 114 | 115 | .noneIcon { 116 | color: var(--vscode-editor-foreground); 117 | } 118 | 119 | .todoStatusIcon { 120 | color: var(--vscode-editor-foreground); 121 | } 122 | 123 | .falsePositiveStatusIcon { 124 | color: var(--vscode-testing-iconPassed); 125 | } 126 | 127 | .bugStatusIcon { 128 | color: var(--vscode-editorError-foreground); 129 | } 130 | 131 | .currentStatus { 132 | text-decoration: underline !important; 133 | } 134 | 135 | .inputArea { 136 | width: 95%; 137 | box-sizing: border-box; 138 | border: 1px; 139 | border-radius: 2px; 140 | background-color: var(--vscode-input-background); 141 | color: var(--vscode-input-foreground); 142 | } 143 | 144 | .inputArea:focus { 145 | outline: 1px solid var(--vscode-focusBorder); 146 | } 147 | 148 | .tabContent { 149 | // This -50px is to account for the tabBar that might otherwise make the html element overflow and display a scrollbar 150 | height: calc(100% - 50px); 151 | } 152 | 153 | .verticalResizableContainer { 154 | display: flex; 155 | height: 100%; 156 | flex-direction: column; 157 | } 158 | 159 | .verticalResizableDivider { 160 | width: 100%; 161 | cursor: row-resize; 162 | // This padding increases the clickable area of the divider 163 | padding-top: 5px; 164 | padding-bottom: 5px; 165 | 166 | .verticalResizableDividerVisible { 167 | background-color: var(--vscode-menu-separatorBackground); 168 | height: 1px; 169 | } 170 | } 171 | 172 | $table_border: 0px solid red; 173 | table { 174 | width: 100%; 175 | 176 | text-align: left; 177 | 178 | border: $table_border; 179 | 180 | th, 181 | td { 182 | padding: 5px; 183 | border: $table_border; 184 | } 185 | 186 | tbody { 187 | margin-top: 40px; 188 | } 189 | } 190 | 191 | .mainTable { 192 | width: 100%; 193 | table-layout: fixed; 194 | 195 | thead th:not(.iconCell):hover, 196 | tbody tr:hover { 197 | background-color: var(--vscode-list-hoverBackground); 198 | cursor: pointer; 199 | } 200 | 201 | tbody { 202 | // A row that is selected 203 | tr.selectedRow { 204 | background-color: var(--vscode-list-activeSelectionBackground); 205 | color: var(--vscode-list-activeSelectionForeground); 206 | } 207 | 208 | // Cells with button that disappear when the row is not hovered or selected 209 | .cellWithButtons { 210 | display: flex; 211 | justify-content: space-between; 212 | 213 | padding-top: 3px; 214 | padding-bottom: 0px; 215 | } 216 | 217 | .cellWithButtonsContent { 218 | @extend .ellipsis; 219 | padding: 2px; 220 | } 221 | 222 | .rowButtons { 223 | display: none; 224 | } 225 | 226 | tr:hover .rowButtons, // When the row is hovered 227 | tr.selectedRow .rowButtons // When the row is selected 228 | { 229 | display: flex; 230 | } 231 | 232 | .filteredOutRow { 233 | .iconCell, 234 | .cellWithButtonsContent { 235 | opacity: 0.4; 236 | } 237 | } 238 | 239 | .filteredOutRowNoClick:hover { 240 | cursor: default; 241 | } 242 | } 243 | } 244 | 245 | .mainTable:focus { 246 | outline: 0; 247 | } 248 | 249 | // Style the top navigation and filtering bar 250 | .topBar { 251 | display: flex; 252 | flex-direction: row; 253 | 254 | // This ensures that the filterBar is as far to the right as possible 255 | justify-content: space-between; 256 | 257 | .tabBar { 258 | // Style the buttons that are used to open the tab content 259 | .tabLink { 260 | color: var(--vscode-editor-foreground); 261 | background-color: var(--vscode-editor-background); 262 | float: left; 263 | border: none; 264 | outline: none; 265 | cursor: pointer; 266 | padding: 7px 8px; 267 | transition: 0.1s; 268 | border: 1px solid var(--vscode-tab-border); 269 | } 270 | 271 | // Change background color of buttons on hover 272 | .tabLink:hover { 273 | background-color: var(--vscode-list-hoverBackground); 274 | } 275 | 276 | // Create an active/current tabLink class 277 | .tabLink.active { 278 | border-bottom: 1px solid transparent; 279 | } 280 | } 281 | 282 | $filter-bar-size: 200px; 283 | $filter-menu-side-padding: 5px; 284 | .filterBar { 285 | align-self: center; 286 | display: flex; 287 | 288 | .filterBarContainer { 289 | width: $filter-bar-size; 290 | padding-right: 5px; 291 | 292 | .keywordFilterAndMenuButtonContainer { 293 | width: 100%; 294 | display: flex; 295 | background-color: var(--vscode-input-background); 296 | } 297 | } 298 | 299 | #filterMenu { 300 | width: $filter-bar-size - (2 * $filter-menu-side-padding); 301 | position: absolute; 302 | z-index: 1; 303 | 304 | background: var(--vscode-menu-background); 305 | box-shadow: var(--vscode-widget-shadow) 0px 0px 8px; 306 | padding: $filter-menu-side-padding; 307 | padding-top: 0px; 308 | color: var(--vscode-foreground); 309 | 310 | .filterTopLabel { 311 | padding-top: 5px; 312 | display: block; 313 | } 314 | 315 | .filterLabel { 316 | padding-top: 10px; 317 | display: block; 318 | } 319 | } 320 | } 321 | } 322 | 323 | #resultsPanel { 324 | #resultsTable { 325 | .resultsTableStatusIcon { 326 | margin-top: 2px; 327 | margin-bottom: -2px; 328 | } 329 | 330 | .resultsTableHeader { 331 | display: flex; 332 | } 333 | 334 | .cellContainer { 335 | display: flex; 336 | align-items: center; 337 | 338 | div { 339 | margin-right: 0.2em; 340 | } 341 | } 342 | 343 | .statusCellContainer { 344 | justify-content: right; 345 | } 346 | 347 | .iconCell { 348 | width: 1.2em; 349 | padding-left: 0; 350 | padding-right: 0; 351 | } 352 | 353 | .statusCell { 354 | width: 2.5em; 355 | padding-right: 0; 356 | } 357 | 358 | .resultPathCell { 359 | @extend .ellipsis-beginning; 360 | width: 25%; 361 | } 362 | 363 | .ruleNameCell { 364 | padding-top: 0; 365 | padding-bottom: 0; 366 | } 367 | 368 | .resultMessageCell { 369 | @extend .ellipsis; 370 | } 371 | 372 | .filteredResultsSummary { 373 | @extend .secondaryText; 374 | font-size: smaller; 375 | } 376 | 377 | tr.selectedRow { 378 | outline: 1px solid var(--vscode-list-focusOutline); 379 | outline-offset: -1px; 380 | } 381 | } 382 | 383 | #resultContextMenu { 384 | position: fixed; 385 | z-index: 10000; 386 | width: 350px; 387 | 388 | // Color not quite right. #322a2a is better 389 | background: var(--vscode-checkbox-background); 390 | 391 | // Color not quite right. #6e6e6e is better 392 | outline: 1px solid var(--vscode-checkbox-border); // #6e6e6e; 393 | outline-offset: -1px; 394 | 395 | box-shadow: 1px; 396 | 397 | font-size: 13px; 398 | 399 | padding: 5px; 400 | border-radius: 5px; 401 | 402 | .resultContextMenuItem { 403 | padding: 5px; 404 | border-radius: 5px; 405 | cursor: pointer; 406 | } 407 | 408 | .resultContextMenuItem:hover { 409 | // Color not quite right. #0176e0 is better 410 | background: var(--vscode-button-background); 411 | } 412 | } 413 | } 414 | 415 | .detailsTable { 416 | width: 100%; 417 | table-layout: fixed; 418 | } 419 | 420 | .detailValueContainer { 421 | display: flex; 422 | 423 | span { 424 | margin-right: 0.2em; 425 | } 426 | } 427 | 428 | .detailValue { 429 | vertical-align: top; 430 | text-align: left; 431 | font-weight: normal; 432 | padding-left: 5px; 433 | } 434 | 435 | .detailKey { 436 | vertical-align: top; 437 | text-align: right; 438 | font-weight: lighter; 439 | width: 75px; 440 | } 441 | 442 | .detailTableKey { 443 | vertical-align: top; 444 | text-align: right; 445 | font-weight: normal; 446 | width: 50px; 447 | } 448 | 449 | .detailTableRow:focus { 450 | outline: 1px solid var(--vscode-list-focusOutline); 451 | outline-offset: -1px; 452 | background-color: var(--vscode-list-activeSelectionBackground) !important; 453 | color: var(--vscode-list-activeSelectionForeground); 454 | } 455 | 456 | .detailEditableTextArea { 457 | width: 100%; 458 | resize: vertical; 459 | } 460 | 461 | #resultDetailsPanel { 462 | .rowButtons { 463 | display: flex; 464 | justify-content: flex-start; 465 | } 466 | 467 | #resultDetailsButtons { 468 | // top | right | bottom | left 469 | padding: 5px 5px 10px 5px; 470 | } 471 | } 472 | 473 | // style the log tab 474 | #sarifFileListPanel { 475 | #sarifFileTableBody { 476 | .fakeCell { 477 | width: 0px; 478 | overflow: hidden; 479 | padding-left: 0px; 480 | padding-right: 0px; 481 | } 482 | } 483 | } 484 | 485 | #noFilesOpenedContainer { 486 | display: flex; 487 | align-items: flex-start; 488 | justify-content: center; 489 | padding-top: 20px; 490 | 491 | #noFilesOpenedButton { 492 | width: 100%; 493 | max-width: 300px; 494 | line-height: 18px; 495 | padding: 4px; 496 | border-radius: 2px; 497 | border: 1px solid var(--vscode-button-border, transparent); 498 | text-align: center; 499 | cursor: pointer; 500 | color: var(--vscode-button-foreground); 501 | background-color: var(--vscode-button-background); 502 | } 503 | 504 | #noFilesOpenedButton:hover { 505 | background-color: var(--vscode-button-hoverBackground); 506 | } 507 | } 508 | 509 | // ==================== 510 | // Remove the overlay once the CSS has loaded 511 | // Keep this at the bottom of the CSS file 512 | #loadOverlay { 513 | display: none; 514 | } 515 | -------------------------------------------------------------------------------- /src/webviewSrc/tabs.ts: -------------------------------------------------------------------------------- 1 | export class TabManager { 2 | private resultsTabButton: HTMLTableElement | undefined = undefined; 3 | private sarifFilesTabButton: HTMLTableElement | undefined = undefined; 4 | 5 | constructor() { 6 | this.initTabs(); 7 | this.showSarifFilesTab(); 8 | } 9 | 10 | private initTabs() { 11 | const maybeResultsButton = document.getElementById("resultsTabButton"); 12 | if (!maybeResultsButton) { 13 | console.error("[SARIF Explorer] results button not found in the document"); 14 | return; 15 | } 16 | this.resultsTabButton = maybeResultsButton as HTMLTableElement; 17 | this.resultsTabButton.onclick = (event) => { 18 | this.showTab(event, "resultsTab"); 19 | }; 20 | 21 | const maybeSarifFilesTabButton = document.getElementById("sarifFilesTabButton"); 22 | if (!maybeSarifFilesTabButton) { 23 | console.error("[SARIF Explorer] logs button not found in the document"); 24 | return; 25 | } 26 | this.sarifFilesTabButton = maybeSarifFilesTabButton as HTMLTableElement; 27 | this.sarifFilesTabButton.onclick = (event) => { 28 | this.showTab(event, "sarifFilesTab"); 29 | }; 30 | } 31 | 32 | private showTab(event: Event, tabName: string) { 33 | if (!event) { 34 | console.error("[SARIF Explorer] showTab called without a valid event object"); 35 | return; 36 | } 37 | 38 | // Hide all elements with class="tabContent" 39 | const tabContent = document.getElementsByClassName("tabContent"); 40 | for (let i = 0; i < tabContent.length; i++) { 41 | const element = tabContent[i] as HTMLElement; 42 | // set the tab to be visible if it's the one we want to show 43 | if (element.id === tabName) { 44 | // The empty string makes it default to the original display value 45 | element.classList.remove("hidden_tab"); 46 | } else { 47 | element.classList.add("hidden_tab"); 48 | } 49 | } 50 | 51 | // Remove "active" from elements with class="tabLink" 52 | const tabLinks = document.getElementsByClassName("tabLink"); 53 | for (let i = 0; i < tabLinks.length; i++) { 54 | tabLinks[i].classList.remove("active"); 55 | } 56 | 57 | // Add "active" to the current tab 58 | const element = event.target as HTMLElement; 59 | element.classList.add("active"); 60 | } 61 | 62 | // ==================== 63 | // Public functions 64 | // ==================== 65 | public showResultsTab() { 66 | if (!this.resultsTabButton) { 67 | console.error("[SARIF Explorer] results button not found in the document"); 68 | return; 69 | } 70 | this.resultsTabButton.click(); 71 | } 72 | 73 | public showSarifFilesTab() { 74 | if (!this.sarifFilesTabButton) { 75 | console.error("[SARIF Explorer] logs button not found in the document"); 76 | return; 77 | } 78 | this.sarifFilesTabButton.click(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/webviewSrc/utils.ts: -------------------------------------------------------------------------------- 1 | export function getElementByIdOrThrow(id: string): HTMLElement { 2 | const el = document.getElementById(id); 3 | if (el === null) { 4 | throw new Error(`${id} not found in the document`); 5 | } 6 | return el; 7 | } 8 | 9 | export function scrollToRow(row: HTMLElement) { 10 | row.scrollIntoView({ block: "nearest", inline: "center" }); 11 | } 12 | -------------------------------------------------------------------------------- /src/webviewSrc/vscode.ts: -------------------------------------------------------------------------------- 1 | // Get the vscode API 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | declare const acquireVsCodeApi: any; 4 | export const vscode = acquireVsCodeApi(); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": ["ES2020", "dom"], 6 | // This setting cause the .js and .js.map files to be created. This enables the better debugging. (I think: https://code.visualstudio.com/docs/typescript/typescript-debugging) 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */, 10 | /* Additional Checks */ 11 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 12 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 13 | "noUnusedParameters": true /* Report errors on unused parameters. */ 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | /* eslint-env node */ 3 | 4 | "use strict"; 5 | 6 | /* trunk-ignore(eslint/@typescript-eslint/no-var-requires) */ 7 | const path = require("path"); 8 | 9 | //@ts-check 10 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 11 | 12 | const config = { 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | exclude: /node_modules/, 18 | use: ["ts-loader"], 19 | }, 20 | { 21 | test: /\.scss$/i, 22 | use: ["style-loader", "css-loader", "sass-loader"], 23 | }, 24 | { 25 | test: /\.(html)$/i, 26 | type: "asset/source", 27 | }, 28 | ], 29 | }, 30 | devtool: "nosources-source-map", 31 | infrastructureLogging: { 32 | level: "log", // enables logging required for problem matchers 33 | }, 34 | resolve: { 35 | extensions: [".ts", ".js", ".scss", ".html"], 36 | }, 37 | externals: { 38 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 39 | // modules added here also need to be added in the .vscodeignore file 40 | }, 41 | }; 42 | 43 | const extensionConfig = Object.assign({}, config, { 44 | target: "node", // VS Code extensions run in a Node.js-context 45 | entry: "./src/extension/extension.ts", // the entry point of the extension 46 | output: { 47 | path: path.resolve(__dirname, "dist"), 48 | filename: "extension.js", 49 | libraryTarget: "commonjs2", 50 | }, 51 | }); 52 | 53 | const webviewConfig = Object.assign({}, config, { 54 | target: "web", // The Webview runs in web context 55 | entry: "./src/webviewSrc/main.ts", // the entry point of the webview 56 | output: { 57 | path: path.resolve(__dirname, "dist"), 58 | filename: "webview.js", 59 | }, 60 | }); 61 | 62 | module.exports = [extensionConfig, webviewConfig]; 63 | --------------------------------------------------------------------------------