├── .envrc ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .github └── workflows │ ├── ci-scan.yaml │ ├── ci.yaml │ └── scan.yaml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── README.v3.md ├── action.yml ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── flake.lock ├── flake.nix ├── index.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── action.ts ├── report.ts ├── sarif.ts ├── scanner.ts └── summary.ts ├── tests ├── filterPackages.test.ts ├── fixtures │ ├── report-test.json │ └── sarif-test.json ├── index.test.ts └── sarif.test.ts └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | has nix && use flake 2 | dotenv_if_exists .env # You can create a .env file with your env vars for this project. You can also use .secrets if you are using act. See the line below. 3 | dotenv_if_exists .secrets # Used by [act](https://nektosact.com/) to load secrets into the pipelines 4 | 5 | export GITHUB_STEP_SUMMARY=/tmp/github_summary.html 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | es6: true 4 | es2021: true 5 | jest: true 6 | node: true 7 | extends: 'eslint:recommended' 8 | globals: 9 | Atomics: "readonly" 10 | SharedArrayBuffer: "readonly" 11 | parserOptions: 12 | ecmaVersion: 2018 13 | rules: {} 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true -------------------------------------------------------------------------------- /.github/workflows/ci-scan.yaml: -------------------------------------------------------------------------------- 1 | name: Scan Image on PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | scan-from-registry: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | # This step checks out a copy of your repository. 12 | - name: Check out repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Scan dummy-vuln-app from registry 16 | id: scan 17 | uses: ./ 18 | continue-on-error: true 19 | with: 20 | # Tag of the image to analyse 21 | image-tag: sysdiglabs/dummy-vuln-app:latest 22 | # API token for Sysdig Scanning auth 23 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 24 | stop-on-failed-policy-eval: true 25 | stop-on-processing-error: true 26 | severity-at-least: medium 27 | 28 | - name: Upload SARIF file 29 | if: success() || failure() # Upload results regardless previous step fails 30 | uses: github/codeql-action/upload-sarif@v3 31 | with: 32 | sarif_file: ${{ github.workspace }}/sarif.json 33 | 34 | - name: Check that the scan has failed 35 | run: | 36 | if [ "${{ steps.scan.outcome }}" == "success" ]; then 37 | echo "Scan succeeded but the step should fail." 38 | exit 1 39 | else 40 | echo "Scan failed as expected." 41 | fi 42 | 43 | filtered-scan-from-registry: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | # This step checks out a copy of your repository. 48 | - name: Check out repository 49 | uses: actions/checkout@v4 50 | 51 | - name: Scan dummy-vuln-app from registry 52 | id: scan 53 | uses: ./ 54 | continue-on-error: true 55 | with: 56 | # Tag of the image to analyse 57 | image-tag: sysdiglabs/dummy-vuln-app:latest 58 | # API token for Sysdig Scanning auth 59 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 60 | stop-on-failed-policy-eval: true 61 | stop-on-processing-error: true 62 | severity-at-least: medium 63 | group-by-package: true 64 | 65 | - name: Upload SARIF file 66 | if: success() || failure() # Upload results regardless previous step fails 67 | uses: github/codeql-action/upload-sarif@v3 68 | with: 69 | sarif_file: ${{ github.workspace }}/sarif.json 70 | 71 | - name: Check that the scan has failed 72 | run: | 73 | if [ "${{ steps.scan.outcome }}" == "success" ]; then 74 | echo "Scan succeeded but the step should fail." 75 | exit 1 76 | else 77 | echo "Scan failed as expected." 78 | fi 79 | 80 | scan-with-old-scanner-version: 81 | runs-on: ubuntu-latest 82 | 83 | steps: 84 | # This step checks out a copy of your repository. 85 | - name: Check out repository 86 | uses: actions/checkout@v4 87 | 88 | - name: Scan dummy-vuln-app from registry 89 | id: scan 90 | uses: ./ 91 | continue-on-error: true 92 | with: 93 | # Old scanner version 94 | cli-scanner-version: 1.8.1 95 | # Tag of the image to analyse 96 | image-tag: sysdiglabs/dummy-vuln-app:latest 97 | # API token for Sysdig Scanning auth 98 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 99 | stop-on-failed-policy-eval: true 100 | stop-on-processing-error: true 101 | severity-at-least: medium 102 | 103 | - name: Upload SARIF file 104 | if: success() || failure() # Upload results regardless previous step fails 105 | uses: github/codeql-action/upload-sarif@v3 106 | with: 107 | sarif_file: ${{ github.workspace }}/sarif.json 108 | 109 | - name: Check that the scan has failed 110 | run: | 111 | if [ "${{ steps.scan.outcome }}" == "success" ]; then 112 | echo "Scan succeeded but the step should fail." 113 | exit 1 114 | else 115 | echo "Scan failed as expected." 116 | fi 117 | 118 | standalone-scan-from-registry: 119 | runs-on: ubuntu-latest 120 | 121 | steps: 122 | # This step checks out a copy of your repository. 123 | - name: Check out repository 124 | uses: actions/checkout@v4 125 | 126 | - name: Donate MainDB from scan 127 | id: donnor-scan 128 | uses: ./ 129 | with: 130 | # Tag of the image to analyse 131 | image-tag: sysdiglabs/dummy-vuln-app:latest 132 | # API token for Sysdig Scanning auth 133 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 134 | stop-on-failed-policy-eval: false 135 | stop-on-processing-error: true 136 | skip-summary: true 137 | 138 | - name: Scan dummy-vuln-app from registry 139 | id: scan 140 | uses: ./ 141 | with: 142 | # Tag of the image to analyse 143 | image-tag: sysdiglabs/dummy-vuln-app:latest 144 | # API token for Sysdig Scanning auth 145 | #sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 146 | stop-on-failed-policy-eval: true 147 | stop-on-processing-error: true 148 | standalone: true 149 | 150 | - name: Upload SARIF file 151 | if: success() || failure() # Upload results regardless previous step fails 152 | uses: github/codeql-action/upload-sarif@v3 153 | with: 154 | sarif_file: ${{ github.workspace }}/sarif.json 155 | 156 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: npm ci 11 | - run: npm test -------------------------------------------------------------------------------- /.github/workflows/scan.yaml: -------------------------------------------------------------------------------- 1 | name: Scan Image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | scan-from-registry: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | # This step checks out a copy of your repository. 12 | - name: Check out repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Scan dummy-vuln-app from registry 16 | id: scan 17 | uses: ./ 18 | with: 19 | # Tag of the image to analyse 20 | image-tag: sysdiglabs/dummy-vuln-app:latest 21 | # API token for Sysdig Scanning auth 22 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 23 | stop-on-failed-policy-eval: true 24 | stop-on-processing-error: true 25 | severity-at-least: medium 26 | 27 | - name: Upload SARIF file 28 | if: success() || failure() # Upload results regardless previous step fails 29 | uses: github/codeql-action/upload-sarif@v3 30 | with: 31 | sarif_file: ${{ github.workspace }}/sarif.json 32 | 33 | filtered-scan-from-registry: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | # This step checks out a copy of your repository. 38 | - name: Check out repository 39 | uses: actions/checkout@v4 40 | 41 | - name: Scan dummy-vuln-app from registry 42 | id: scan 43 | uses: ./ 44 | with: 45 | # Tag of the image to analyse 46 | image-tag: sysdiglabs/dummy-vuln-app:latest 47 | # API token for Sysdig Scanning auth 48 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 49 | stop-on-failed-policy-eval: true 50 | stop-on-processing-error: true 51 | severity-at-least: medium 52 | group-by-package: true 53 | 54 | - name: Upload SARIF file 55 | if: success() || failure() # Upload results regardless previous step fails 56 | uses: github/codeql-action/upload-sarif@v3 57 | with: 58 | sarif_file: ${{ github.workspace }}/sarif.json 59 | 60 | standalone-scan-from-registry: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | # This step checks out a copy of your repository. 65 | - name: Check out repository 66 | uses: actions/checkout@v4 67 | 68 | - name: Donate MainDB from scan 69 | id: donnor-scan 70 | uses: ./ 71 | with: 72 | # Tag of the image to analyse 73 | image-tag: sysdiglabs/dummy-vuln-app:latest 74 | # API token for Sysdig Scanning auth 75 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 76 | stop-on-failed-policy-eval: false 77 | stop-on-processing-error: true 78 | skip-summary: true 79 | 80 | - name: Scan dummy-vuln-app from registry 81 | id: scan 82 | uses: ./ 83 | with: 84 | # Tag of the image to analyse 85 | image-tag: sysdiglabs/dummy-vuln-app:latest 86 | # API token for Sysdig Scanning auth 87 | #sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 88 | stop-on-failed-policy-eval: true 89 | stop-on-processing-error: true 90 | standalone: true 91 | 92 | - name: Upload SARIF file 93 | if: success() || failure() # Upload results regardless previous step fails 94 | uses: github/codeql-action/upload-sarif@v3 95 | with: 96 | sarif_file: ${{ github.workspace }}/sarif.json 97 | 98 | scan-image-without-vulns: 99 | runs-on: ubuntu-latest 100 | 101 | steps: 102 | # This step checks out a copy of your repository. 103 | - name: Check out repository 104 | uses: actions/checkout@v4 105 | 106 | - name: Scan hello-world from registry 107 | id: scan 108 | uses: ./ 109 | with: 110 | # Tag of the image to analyse 111 | image-tag: hello-world:latest # This one should never have vulns 112 | # API token for Sysdig Scanning auth 113 | sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }} 114 | stop-on-failed-policy-eval: true 115 | stop-on-processing-error: true 116 | severity-at-least: medium 117 | group-by-package: true 118 | 119 | - name: Upload SARIF file 120 | if: success() || failure() # Upload results regardless previous step fails 121 | uses: github/codeql-action/upload-sarif@v3 122 | with: 123 | sarif_file: ${{ github.workspace }}/sarif.json 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,node,visualstudiocode,jetbrains+all,direnv 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,node,visualstudiocode,jetbrains+all,direnv 3 | 4 | ### direnv ### 5 | .direnv 6 | .envrc 7 | 8 | ### JetBrains+all ### 9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 11 | 12 | # User-specific stuff 13 | .idea/**/workspace.xml 14 | .idea/**/tasks.xml 15 | .idea/**/usage.statistics.xml 16 | .idea/**/dictionaries 17 | .idea/**/shelf 18 | 19 | # AWS User-specific 20 | .idea/**/aws.xml 21 | 22 | # Generated files 23 | .idea/**/contentModel.xml 24 | 25 | # Sensitive or high-churn files 26 | .idea/**/dataSources/ 27 | .idea/**/dataSources.ids 28 | .idea/**/dataSources.local.xml 29 | .idea/**/sqlDataSources.xml 30 | .idea/**/dynamic.xml 31 | .idea/**/uiDesigner.xml 32 | .idea/**/dbnavigator.xml 33 | 34 | # Gradle 35 | .idea/**/gradle.xml 36 | .idea/**/libraries 37 | 38 | # Gradle and Maven with auto-import 39 | # When using Gradle or Maven with auto-import, you should exclude module files, 40 | # since they will be recreated, and may cause churn. Uncomment if using 41 | # auto-import. 42 | # .idea/artifacts 43 | # .idea/compiler.xml 44 | # .idea/jarRepositories.xml 45 | # .idea/modules.xml 46 | # .idea/*.iml 47 | # .idea/modules 48 | # *.iml 49 | # *.ipr 50 | 51 | # CMake 52 | cmake-build-*/ 53 | 54 | # Mongo Explorer plugin 55 | .idea/**/mongoSettings.xml 56 | 57 | # File-based project format 58 | *.iws 59 | 60 | # IntelliJ 61 | out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Cursive Clojure plugin 70 | .idea/replstate.xml 71 | 72 | # SonarLint plugin 73 | .idea/sonarlint/ 74 | 75 | # Crashlytics plugin (for Android Studio and IntelliJ) 76 | com_crashlytics_export_strings.xml 77 | crashlytics.properties 78 | crashlytics-build.properties 79 | fabric.properties 80 | 81 | # Editor-based Rest Client 82 | .idea/httpRequests 83 | 84 | # Android studio 3.1+ serialized cache file 85 | .idea/caches/build_file_checksums.ser 86 | 87 | ### JetBrains+all Patch ### 88 | # Ignore everything but code style settings and run configurations 89 | # that are supposed to be shared within teams. 90 | 91 | .idea/* 92 | 93 | !.idea/codeStyles 94 | !.idea/runConfigurations 95 | 96 | ### Linux ### 97 | *~ 98 | 99 | # temporary files which can be created if a process still has a handle open of a deleted file 100 | .fuse_hidden* 101 | 102 | # KDE directory preferences 103 | .directory 104 | 105 | # Linux trash folder which might appear on any partition or disk 106 | .Trash-* 107 | 108 | # .nfs files are created when an open file is removed but is still being accessed 109 | .nfs* 110 | 111 | ### macOS ### 112 | # General 113 | .DS_Store 114 | .AppleDouble 115 | .LSOverride 116 | 117 | # Icon must end with two \r 118 | Icon 119 | 120 | 121 | # Thumbnails 122 | ._* 123 | 124 | # Files that might appear in the root of a volume 125 | .DocumentRevisions-V100 126 | .fseventsd 127 | .Spotlight-V100 128 | .TemporaryItems 129 | .Trashes 130 | .VolumeIcon.icns 131 | .com.apple.timemachine.donotpresent 132 | 133 | # Directories potentially created on remote AFP share 134 | .AppleDB 135 | .AppleDesktop 136 | Network Trash Folder 137 | Temporary Items 138 | .apdisk 139 | 140 | ### macOS Patch ### 141 | # iCloud generated files 142 | *.icloud 143 | 144 | ### Node ### 145 | # Logs 146 | logs 147 | *.log 148 | npm-debug.log* 149 | yarn-debug.log* 150 | yarn-error.log* 151 | lerna-debug.log* 152 | .pnpm-debug.log* 153 | 154 | # Diagnostic reports (https://nodejs.org/api/report.html) 155 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 156 | 157 | # Runtime data 158 | pids 159 | *.pid 160 | *.seed 161 | *.pid.lock 162 | 163 | # Directory for instrumented libs generated by jscoverage/JSCover 164 | lib-cov 165 | 166 | # Coverage directory used by tools like istanbul 167 | coverage 168 | *.lcov 169 | 170 | # nyc test coverage 171 | .nyc_output 172 | 173 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 174 | .grunt 175 | 176 | # Bower dependency directory (https://bower.io/) 177 | bower_components 178 | 179 | # node-waf configuration 180 | .lock-wscript 181 | 182 | # Compiled binary addons (https://nodejs.org/api/addons.html) 183 | build/Release 184 | 185 | # Dependency directories 186 | node_modules/ 187 | jspm_packages/ 188 | 189 | # Snowpack dependency directory (https://snowpack.dev/) 190 | web_modules/ 191 | 192 | # TypeScript cache 193 | *.tsbuildinfo 194 | 195 | # Optional npm cache directory 196 | .npm 197 | 198 | # Optional eslint cache 199 | .eslintcache 200 | 201 | # Optional stylelint cache 202 | .stylelintcache 203 | 204 | # Microbundle cache 205 | .rpt2_cache/ 206 | .rts2_cache_cjs/ 207 | .rts2_cache_es/ 208 | .rts2_cache_umd/ 209 | 210 | # Optional REPL history 211 | .node_repl_history 212 | 213 | # Output of 'npm pack' 214 | *.tgz 215 | 216 | # Yarn Integrity file 217 | .yarn-integrity 218 | 219 | # dotenv environment variable files 220 | .env 221 | .env.development.local 222 | .env.test.local 223 | .env.production.local 224 | .env.local 225 | 226 | # parcel-bundler cache (https://parceljs.org/) 227 | .cache 228 | .parcel-cache 229 | 230 | # Next.js build output 231 | .next 232 | out 233 | 234 | # Nuxt.js build / generate output 235 | .nuxt 236 | dist 237 | 238 | # Gatsby files 239 | .cache/ 240 | # Comment in the public line in if your project uses Gatsby and not Next.js 241 | # https://nextjs.org/blog/next-9-1#public-directory-support 242 | # public 243 | 244 | # vuepress build output 245 | .vuepress/dist 246 | 247 | # vuepress v2.x temp and cache directory 248 | .temp 249 | 250 | # Docusaurus cache and generated files 251 | .docusaurus 252 | 253 | # Serverless directories 254 | .serverless/ 255 | 256 | # FuseBox cache 257 | .fusebox/ 258 | 259 | # DynamoDB Local files 260 | .dynamodb/ 261 | 262 | # TernJS port file 263 | .tern-port 264 | 265 | # Stores VSCode versions used for testing VSCode extensions 266 | .vscode-test 267 | 268 | # yarn v2 269 | .yarn/cache 270 | .yarn/unplugged 271 | .yarn/build-state.yml 272 | .yarn/install-state.gz 273 | .pnp.* 274 | 275 | ### Node Patch ### 276 | # Serverless Webpack directories 277 | .webpack/ 278 | 279 | # Optional stylelint cache 280 | 281 | # SvelteKit build / generate output 282 | .svelte-kit 283 | 284 | ### VisualStudioCode ### 285 | .vscode/* 286 | !.vscode/settings.json 287 | !.vscode/tasks.json 288 | !.vscode/launch.json 289 | !.vscode/extensions.json 290 | !.vscode/*.code-snippets 291 | 292 | # Local History for Visual Studio Code 293 | .history/ 294 | 295 | # Built Visual Studio Code Extensions 296 | *.vsix 297 | 298 | ### VisualStudioCode Patch ### 299 | # Ignore all local history of files 300 | .history 301 | .ionide 302 | 303 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,node,visualstudiocode,jetbrains+all,direnv 304 | 305 | report.json 306 | sarif.json 307 | build/ 308 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sysdiglabs/sysdig-training 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sysdig Secure Inline Scan Action 2 | 3 | > 🚧 **Warning**: To use the Legacy Scanning Engine Action, please use version v3.* and visit the [previous README](./README.v3.md). 4 | 5 | This action performs analysis on a specific container image and posts the result to Sysdig Secure. For more information about Sysdig CLI Scanner, see [Sysdig Secure documentation](https://docs.sysdig.com/en/docs/installation/sysdig-secure/install-vulnerability-cli-scanner/running-in-vm-mode/). 6 | 7 | ## Inputs 8 | 9 | | Input | Description | Default | 10 | |------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| 11 | | `cli-scanner-url` | URL to `sysdig-cli-scanner` binary download. The action will detect the runner OS and architecture. For more info about the Sysdig CLI Scanner download visit [the official documentation](https://docs.sysdig.com/en/docs/installation/sysdig-secure/install-vulnerability-cli-scanner/). | | 12 | | `mode` | Mode of operation. Can be "vm" or "iac". | `vm` | 13 | | `cli-scanner-version` | Custom sysdig-cli-scanner version to download. If using iac mode, minimum required version is 1.9.0. Please note that for VM mode the Action has only been tested with the current default version and it is not guaranteed that it will work as expected with other versions. | `1.22.1` | 14 | | `registry-user` | Registry username to authenticate to while pulling the image to scan. | | 15 | | `registry-password` | Registry password to authenticate to while pulling the image to scan. | | 16 | | `stop-on-failed-policy-eval` | Fail the job if the Policy Evaluation is Failed. | | 17 | | `stop-on-processing-error` | Fail the job if the Scanner terminates execution with errors. | | 18 | | `severity-at-least` | Filtering option to only report vulnerabilities with at least the specified severity. Can take `critical`, `high`, `medium`, `low`, `negligible` or `any`. Default value "any" for no filtering. For example, if `severity-at-least` is set to `medium`, only Medium, High or Critical vulnerabilities will be reported. | `any` | 19 | | `package-types` | Comma-separated list of package types to include in the report (e.g. `java,javascript`). Only vulnerabilities found in these types of packages will be included. If empty, no inclusion filter is applied. | | 20 | | `not-package-types` | Comma-separated list of package types to exclude from the report (e.g. `os`). Vulnerabilities found in these types of packages will be excluded. If empty, no exclusion filter is applied. | | 21 | | `exclude-accepted` | Set to `true` to exclude vulnerabilities that have accepted risks (`acceptedRisks`). Useful to focus only on unresolved findings. | `false` | 22 | | `group-by-package` | Enable grouping the vulnerabilities in the SARIF report by package. Useful if you want to manage security per package or condense the number of findings. | | 23 | | `standalone` | Enable standalone mode. Do not depend on Sysdig backend for execution, avoiding the need of specifying 'sysdig-secure-token' and 'sysdig-secure-url'. Recommended when using runners with no access to the internet. May require to specify custom `cli-scanner-url` and `db-path`. | | 24 | | `db-path` | Specify the directory for the vulnerabilities database to use while scanning. Useful when running in standalone mode. | | 25 | | `skip-upload` | Skip uploading scanning results to Sysdig Secure. | | 26 | | `skip-summary` | Skip generating Summary. | | 27 | | `use-policies` | Specify Sysdig Secure VM Policies to evaluate the image. | | 28 | | `override-pullstring` | Custom PullString to give the image when scanning and uploading. Useful when building images in a pipeline with temporary names. The custom PullString will be used to identify the scanned image in Sysdig Secure. | | 29 | | `image-tag` | Tag of the image to analyse. | | 30 | | `sysdig-secure-token` | API token for Sysdig Scanning authentication. (Required if not in Standalone mode.) | | 31 | | `sysdig-secure-url` | Sysdig Secure Endpoint URL. Defaults to `https://secure.sysdig.com`. Please, visit the [official documentation](https://docs.sysdig.com/en/docs/administration/saas-regions-and-ip-ranges/) for more details on endpoints and regions. | https://secure.sysdig.com | 32 | | `sysdig-skip-tls` | Skip TLS verification when calling Sysdig Secure endpoints. | | 33 | | `extra-parameters` | Additional parameters to be added to the CLI Scanner. Note that these may not be supported with the current Action. | | 34 | | `recursive` | Recursively scan all folders within the folder specified in the iacScanPath. | | 35 | | `minimum-severity` | Minimum severity to fail when scanning in IaC mode. | | 36 | | `iac-scan-path` | Path to the IaC files to scan. | | 37 | 38 | ### Filtering Examples 39 | 40 | - **severity-at-least:** 41 | `medium` → Only Medium, High, and Critical findings will be reported. 42 | 43 | - **package-types:** 44 | `java,javascript` → Only vulnerabilities in Java or JavaScript packages will be included. 45 | 46 | - **not-package-types:** 47 | `os` → Excludes vulnerabilities found in OS packages. 48 | 49 | - **exclude-accepted:** 50 | `true` → Vulnerabilities that are marked as "accepted" (i.e., with risk acceptances) are excluded from the report. 51 | 52 | > ℹ️ You can combine these filters to focus the report on just what you care about! 53 | 54 | --- 55 | 56 | ## SARIF Report 57 | 58 | The action generates a SARIF report that can be uploaded using the `codeql-action/upload-sarif` action. 59 | 60 | You need to assign an ID to the Sysdig Scan Action step, like: 61 | 62 | ```yaml 63 | ... 64 | 65 | - name: Scan image 66 | id: scan 67 | uses: sysdiglabs/scan-action@v5 68 | with: 69 | ... 70 | ``` 71 | 72 | and then add another step for uploading the SARIF report, providing the path in the `sarif_file` parameter: 73 | 74 | ```yaml 75 | ... 76 | - name: Upload SARIF file 77 | if: success() || failure() 78 | uses: github/codeql-action/upload-sarif@v3 79 | with: 80 | sarif_file: ${{ github.workspace }}/sarif.json 81 | ``` 82 | 83 | The `if: success() || failure()` option makes sure the SARIF report is uploaded even if the scan fails and interrupts the workflow. (Q: Why not `always()`? A: That would allow for canceled jobs as well.) 84 | 85 | ## Example usages 86 | 87 | ### Build and scan image locally using Docker, and upload SARIF report 88 | 89 | ```yaml 90 | 91 | ... 92 | 93 | - name: Build the Docker image 94 | run: docker build . --file Dockerfile --tag sysdiglabs/dummy-vuln-app:latest 95 | 96 | - name: Scan image 97 | id: scan 98 | uses: sysdiglabs/scan-action@v5 99 | with: 100 | image-tag: sysdiglabs/dummy-vuln-app:latest 101 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 102 | 103 | - name: Upload SARIF file 104 | if: success() || failure() 105 | uses: github/codeql-action/upload-sarif@v3 106 | with: 107 | sarif_file: ${{ github.workspace }}/sarif.json 108 | 109 | ``` 110 | 111 | ### Pull and scan an image from a registry 112 | 113 | ```yaml 114 | ... 115 | 116 | - name: Scan image 117 | uses: sysdiglabs/scan-action@v5 118 | with: 119 | image-tag: "sysdiglabs/dummy-vuln-app:latest" 120 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 121 | ``` 122 | 123 | ### Scan infrastructure using IaC scan 124 | 125 | ```yaml 126 | ... 127 | 128 | - name: Scan infrastructure 129 | uses: sysdiglabs/scan-action@v5 130 | with: 131 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 132 | cli-scanner-version: 1.9.0 133 | mode: iac 134 | iac-scan-path: ./terraform 135 | ``` 136 | 137 | ### Fail pipeline when Policy Evaluation is failed or scanner fails to run 138 | 139 | 140 | ```yaml 141 | ... 142 | 143 | - name: Scan image 144 | uses: sysdiglabs/scan-action@v3 145 | with: 146 | image-tag: "sysdiglabs/dummy-vuln-app:latest" 147 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 148 | stop-on-failed-policy-eval: true 149 | stop-on-processing-error: true 150 | ``` 151 | -------------------------------------------------------------------------------- /README.v3.md: -------------------------------------------------------------------------------- 1 | 2 | # Sysdig Secure Inline Scan Action 3 | 4 | This action performs analysis on locally built container image and posts the result to Sysdig Secure. For more information about Secure Inline Scan, see [Sysdig Secure documentation](https://docs.sysdig.com/en/integrate-with-ci-cd-tools.html). 5 | 6 | ## Inputs 7 | 8 | ### `image-tag` 9 | 10 | **Required** The tag of the local image to scan. Example: `"sysdiglabs/dummy-vuln-app:latest"`. 11 | 12 | ### `sysdig-secure-token` 13 | 14 | **Required** API token for Sysdig Scanning auth. Example: `"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`. 15 | 16 | Directly specifying the API token in the action configuration is not recommended. A better approach is to [store it in GitHub secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets), and reference `${{ secrets.MY_SECRET_NAME }}` instead. 17 | 18 | ### `sysdig-secure-url` 19 | 20 | Sysdig Secure URL. Example: `https://secure-sysdig.svc.cluster.local` 21 | 22 | If not specified, it will default to Sysdig Secure SaaS URL (https://secure.sysdig.com). 23 | 24 | For SaaS, eee [SaaS Regions and IP Ranges](https://docs.sysdig.com/en/saas-regions-and-ip-ranges.html). 25 | 26 | ### `sysdig-skip-tls` 27 | 28 | Skip TLS verification when calling secure endpoints. 29 | 30 | ### `dockerfile-path` 31 | 32 | Path to Dockerfile. Example: `"./Dockerfile"`. 33 | 34 | ### `ignore-failed-scan` 35 | 36 | Don't fail the execution of this action even if the scan result is FAILED. 37 | 38 | ### `input-type` 39 | 40 | If specified, where should we scan the image from. Possible values: 41 | * **pull**: Pull the image from the registry. Default if not specified. 42 | * **docker-daemon**: Get the image from the Docker daemon. The Docker socket must be available at `/var/run/docker.sock` 43 | * **cri-o**: Get the image from containers-storage (CRI-O and others). Images must be stored in `/var/lib/containers` 44 | * docker-archive: Image is provided as a Docker .tar file (from Docker save). Specify the path to the tar file with `input-path` parameter. 45 | * **oci-archive**: Image is provided as a OCI image tar file. Specify the path to the tar file with `input-path` parameter. 46 | * **oci-dir**: Image is provided as a OCI image, untared. Specify the path to the directory file with `input-path` parameter. 47 | 48 | ### `input-path` 49 | 50 | Path to the tar file or OCI layout directory, or the Docker daemon when using `input-type: docker-daemon`, in case the `docker.sock` file is not in the default path `/var/run/docker.sock`. 51 | 52 | ### `run-as-user` 53 | 54 | Run the scan container with this username or UID. 55 | It might be required when scanning from docker-daemon or cri-o to provide a user with permissions on the socket or storage. 56 | 57 | ### `extra-parameters` 58 | 59 | Additional parameters added to the secure-inline-scan container execution. 60 | 61 | ### `extra-docker-parameters` 62 | 63 | Additional parameters added to the `docker` command when executing the secure-inline-scan container execution. 64 | 65 | ### `severity` 66 | 67 | Filter output annotations by severity. Default is "unknown". 68 | Possible values: 69 | - critical 70 | - high 71 | - medium 72 | - negligible 73 | - unknown 74 | 75 | ### `unique-report-by-package` 76 | 77 | Only one annotation by package name/version will be displayed in the build output. 78 | The last highest (by severity) vulnerability will be displayed by package. 79 | It increases the readability of the output, avoiding duplicates for the same package. 80 | Default to false. 81 | 82 | 83 | ### `inline-scan-image` 84 | 85 | The image `quay.io/sysdig/secure-inline-scan:2`, which points to the latest 2.x version of the Sysdig Secure inline scanner is used by default. 86 | This parameter allows overriding the default image, to use a specific version or for air-gapped environments. 87 | 88 | ## SARIF Report 89 | 90 | The action generates a SARIF report that can be uploaded using the `codeql-action/upload-sarif` action. 91 | 92 | You need to assign an ID to the Sysdig Scan Action step, like: 93 | 94 | ```yaml 95 | ... 96 | 97 | - name: Scan image 98 | id: scan 99 | uses: sysdiglabs/scan-action@v3 100 | with: 101 | ... 102 | ``` 103 | 104 | and then add another step for uploading the SARIF report, providing the path in the `sarifReport` output: 105 | 106 | ```yaml 107 | ... 108 | - uses: github/codeql-action/upload-sarif@v1 109 | with: 110 | if: always() 111 | sarif_file: ${{ steps.scan.outputs.sarifReport }} 112 | ``` 113 | 114 | The `if: always()` option makes sure the SARIF report is uploaded even if the scan fails and interrupts the workflow. 115 | 116 | ## Example usages 117 | 118 | ### Build and scan image locally using Docker, and upload SARIF report 119 | 120 | ```yaml 121 | 122 | ... 123 | 124 | - name: Build the Docker image 125 | run: docker build . --file Dockerfile --tag sysdiglabs/dummy-vuln-app:latest 126 | 127 | - name: Scan image 128 | id: scan 129 | uses: sysdiglabs/scan-action@v3 130 | with: 131 | image-tag: "sysdiglabs/dummy-vuln-app:latest" 132 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 133 | input-type: docker-daemon 134 | run-as-user: root 135 | 136 | - uses: github/codeql-action/upload-sarif@v1 137 | if: always() 138 | with: 139 | sarif_file: ${{ steps.scan.outputs.sarifReport }} 140 | 141 | ``` 142 | 143 | ### Pull and scan an image from a registry 144 | 145 | ```yaml 146 | ... 147 | 148 | - name: Scan image 149 | uses: sysdiglabs/scan-action@v3 150 | with: 151 | image-tag: "sysdiglabs/dummy-vuln-app:latest" 152 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 153 | ``` 154 | 155 | ### Scan a Docker archive image 156 | 157 | 158 | ```yaml 159 | ... 160 | 161 | - name: Scan image 162 | uses: sysdiglabs/scan-action@v3 163 | with: 164 | image-tag: "sysdiglabs/dummy-vuln-app:latest" 165 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN }} 166 | input-type: docker-archive 167 | input-path: artifacts/my-image.tar 168 | ``` 169 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Sysdig Secure Inline Scan' 2 | description: 'Perform image analysis on locally built container image and post the result of the analysis to Sysdig Secure.' 3 | inputs: 4 | cli-scanner-url: 5 | description: URL to sysdig-cli-scanner binary download 6 | required: false 7 | cli-scanner-version: 8 | description: Custom sysdig-cli-scanner version to download 9 | default: "1.22.1" 10 | required: false 11 | registry-user: 12 | description: Registry username. 13 | required: false 14 | registry-password: 15 | description: Registry password. 16 | required: false 17 | stop-on-failed-policy-eval: 18 | description: Fail the job if the Policy Evaluation is Failed. 19 | default: "false" 20 | required: false 21 | stop-on-processing-error: 22 | description: Fail the job if the Scanner terminates execution with errors. 23 | default: "false" 24 | required: false 25 | standalone: 26 | description: Enable standalone mode. Do not depend on Sysdig backend for execution, avoiding the need of specifying 'sysdig-secure-token' and 'sysdig-secure-url'. 27 | default: "false" 28 | required: false 29 | db-path: 30 | description: Specify directory for database to use while scanning. 31 | required: false 32 | skip-upload: 33 | description: Skip uploading results to Sysdig Secure. 34 | default: "false" 35 | required: false 36 | skip-summary: 37 | description: Skip generating Summary. 38 | default: "false" 39 | required: false 40 | use-policies: 41 | description: Specify Sysdig Secure VM Policies to evaluate the image. 42 | required: false 43 | override-pullstring: 44 | description: Custom PullString to give the image when scanning and uploading. 45 | required: false 46 | severity-at-least: 47 | description: Filtering option to only report vulnerabilities with at least the specified severity. Can take [critical|high|medium|low|negligible|any]. Default value "any" for no filtering. 48 | default: any 49 | required: false 50 | package-types: 51 | description: "Comma-separated list of package types to include in the SARIF/summary report. Example: \"java,javascript\"" 52 | required: false 53 | not-package-types: 54 | description: "Comma-separated list of package types to exclude from the SARIF/summary report. Example: \"os,alpine\"" 55 | required: false 56 | exclude-accepted: 57 | description: "Exclude vulnerabilities that have accepted risks from SARIF/summary report. true/false" 58 | default: "false" 59 | required: false 60 | group-by-package: 61 | description: Enable grouping the vulnerabilities in the SARIF report by package. 62 | default: "false" 63 | required: false 64 | image-tag: 65 | description: Tag of the image to analyse. (Required if not in IaC mode.) 66 | required: false 67 | sysdig-secure-token: 68 | description: API token for Sysdig Scanning auth. (Required if not in Standalone mode.) 69 | required: false 70 | sysdig-secure-url: 71 | description: 'Sysdig Secure URL (ex: "https://secure-sysdig.com").' 72 | required: false 73 | default: https://secure.sysdig.com 74 | sysdig-skip-tls: 75 | description: Skip TLS verification when calling secure endpoints. 76 | required: false 77 | extra-parameters: 78 | description: Additional parameters added to the secure-inline-scan container execution. 79 | required: false 80 | mode: 81 | description: 'Mode of operation. Can be "vm" or "iac".' 82 | required: false 83 | default: vm 84 | recursive: 85 | description: 'Recursively scan all folders within the folder specified in the iacScanPath.' 86 | required: false 87 | default: "false" 88 | minimum-severity: 89 | description: 'Minimum severity to fail when scanning in IaC mode' 90 | required: false 91 | default: "high" 92 | iac-scan-path: 93 | description: 'Path to the IaC files to scan.' 94 | required: false 95 | default: "./" 96 | outputs: 97 | scanReport: 98 | description: Path to a JSON file containing the report results, failed evaluation gates, and found vulnerabilities. 99 | sarifReport: 100 | description: | 101 | Path to a SARIF report, that can be uploaded using the codeql-action/upload-sarif action. See the README for more information. 102 | 103 | branding: 104 | icon: 'shield' 105 | color: 'orange' 106 | runs: 107 | using: 'node20' 108 | main: 'dist/index.js' 109 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/http-client 26 | MIT 27 | Actions Http Client for Node.js 28 | 29 | Copyright (c) GitHub, Inc. 30 | 31 | All rights reserved. 32 | 33 | MIT License 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 36 | associated documentation files (the "Software"), to deal in the Software without restriction, 37 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 38 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 39 | subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 44 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 45 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 46 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 47 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | 50 | @actions/io 51 | MIT 52 | The MIT License (MIT) 53 | 54 | Copyright 2019 GitHub 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 61 | 62 | @fastify/busboy 63 | MIT 64 | Copyright Brian White. All rights reserved. 65 | 66 | Permission is hereby granted, free of charge, to any person obtaining a copy 67 | of this software and associated documentation files (the "Software"), to 68 | deal in the Software without restriction, including without limitation the 69 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 70 | sell copies of the Software, and to permit persons to whom the Software is 71 | furnished to do so, subject to the following conditions: 72 | 73 | The above copyright notice and this permission notice shall be included in 74 | all copies or substantial portions of the Software. 75 | 76 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 77 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 78 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 79 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 80 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 81 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 82 | IN THE SOFTWARE. 83 | 84 | secure-inline-scan-action 85 | Apache-2.0 86 | 87 | tunnel 88 | MIT 89 | The MIT License (MIT) 90 | 91 | Copyright (c) 2012 Koichi Kobayashi 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy 94 | of this software and associated documentation files (the "Software"), to deal 95 | in the Software without restriction, including without limitation the rights 96 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 97 | copies of the Software, and to permit persons to whom the Software is 98 | furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in 101 | all copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 109 | THE SOFTWARE. 110 | 111 | 112 | undici 113 | MIT 114 | MIT License 115 | 116 | Copyright (c) Matteo Collina and Undici contributors 117 | 118 | Permission is hereby granted, free of charge, to any person obtaining a copy 119 | of this software and associated documentation files (the "Software"), to deal 120 | in the Software without restriction, including without limitation the rights 121 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 122 | copies of the Software, and to permit persons to whom the Software is 123 | furnished to do so, subject to the following conditions: 124 | 125 | The above copyright notice and this permission notice shall be included in all 126 | copies or substantial portions of the Software. 127 | 128 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 129 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 130 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 131 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 132 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 133 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 134 | SOFTWARE. 135 | -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})(); -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1747467164, 6 | "narHash": "sha256-JBXbjJ0t6T6BbVc9iPVquQI9XSXCGQJD8c8SgnUquus=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "3fcbdcfc707e0aa42c541b7743e05820472bdaec", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1731533236, 46 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | utils.url = "github:numtide/flake-utils"; 5 | }; 6 | outputs = { 7 | self, 8 | nixpkgs, 9 | utils, 10 | }: 11 | utils.lib.eachDefaultSystem ( 12 | system: let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | config.allowUnfree = true; 16 | }; 17 | in { 18 | devShells.default = with pkgs; 19 | mkShell { 20 | buildInputs = 21 | [ 22 | typescript 23 | nodejs_20 24 | ] 25 | ++ (with nodePackages; [ 26 | typescript-language-server 27 | eslint 28 | ]); 29 | }; 30 | 31 | formatter = pkgs.alejandra; 32 | } 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import fs from 'fs'; 3 | import { generateSARIFReport } from './src/sarif'; 4 | import { cliScannerName, cliScannerResult, cliScannerURL, executeScan, pullScanner, ScanExecutionResult, ScanMode } from './src/scanner'; 5 | import { ActionInputs, defaultSecureEndpoint } from './src/action'; 6 | import { generateSummary } from './src/summary'; 7 | import { Report, FilterOptions, Severity } from './src/report'; 8 | 9 | function parseCsvList(str?: string): string[] { 10 | if (!str) return []; 11 | return str.split(",").map(s => s.trim()).filter(s => !!s); 12 | } 13 | 14 | export class ExecutionError extends Error { 15 | constructor(stdout: string, stderr: string) { 16 | super("execution error\n\nstdout: " + stdout + "\n\nstderr: " + stderr); 17 | } 18 | } 19 | 20 | function writeReport(reportData: string) { 21 | fs.writeFileSync("./report.json", reportData); 22 | core.setOutput("scanReport", "./report.json"); 23 | } 24 | 25 | export async function run() { 26 | 27 | try { 28 | let opts = ActionInputs.parseActionInputs(); 29 | opts.printOptions(); 30 | let scanFlags = opts.composeFlags(); 31 | 32 | let scanResult: ScanExecutionResult; 33 | // Download CLI Scanner from 'cliScannerURL' 34 | let retCode = await pullScanner(opts.cliScannerURL); 35 | if (retCode == 0) { 36 | // Execute Scanner 37 | scanResult = await executeScan(scanFlags); 38 | 39 | retCode = scanResult.ReturnCode; 40 | if (retCode == 0 || retCode == 1) { 41 | // Transform Scan Results to other formats such as SARIF 42 | if (opts.mode == ScanMode.vm) { 43 | await processScanResult(scanResult, opts); 44 | } 45 | } else { 46 | core.error("Terminating scan. Scanner couldn't be executed.") 47 | } 48 | } else { 49 | core.error("Terminating scan. Scanner couldn't be pulled.") 50 | } 51 | 52 | if (opts.stopOnFailedPolicyEval && retCode == 1) { 53 | core.setFailed(`Stopping because Policy Evaluation was FAILED.`); 54 | } else if (opts.standalone && retCode == 0) { 55 | core.info("Policy Evaluation was OMITTED."); 56 | } else if (retCode == 0) { 57 | core.info("Policy Evaluation was PASSED."); 58 | } else if (opts.stopOnProcessingError && retCode > 1) { 59 | core.setFailed(`Stopping because the scanner terminated with an error.`); 60 | } // else: Don't stop regardless the outcome. 61 | 62 | } catch (error) { 63 | if (core.getInput('stop-on-processing-error') == 'true') { 64 | core.setFailed(`Unexpected error: ${error instanceof Error ? error.stack : String(error)}`); 65 | } 66 | core.error(`Unexpected error: ${error instanceof Error ? error.stack : String(error)}`); 67 | } 68 | } 69 | 70 | export async function processScanResult(result: ScanExecutionResult, opts: ActionInputs) { 71 | writeReport(result.Output); 72 | 73 | let report: Report; 74 | try { 75 | report = JSON.parse(result.Output); 76 | } catch (error) { 77 | core.error("Error parsing analysis JSON report: " + error + ". Output was: " + result.Output); 78 | throw new ExecutionError(result.Output, result.Error); 79 | } 80 | 81 | if (report) { 82 | const filters: FilterOptions = { 83 | minSeverity: (opts.severityAtLeast && opts.severityAtLeast.toLowerCase() !== "any") 84 | ? opts.severityAtLeast.toLowerCase() as Severity 85 | : undefined, 86 | packageTypes: parseCsvList(opts.packageTypes), 87 | notPackageTypes: parseCsvList(opts.notPackageTypes), 88 | excludeAccepted: opts.excludeAccepted, 89 | }; 90 | 91 | core.info("Generating SARIF Report...") 92 | generateSARIFReport(report, opts.groupByPackage, filters); 93 | 94 | if (!opts.skipSummary) { 95 | core.info("Generating Summary...") 96 | await generateSummary(opts, report, filters); 97 | } else { 98 | core.info("Skipping Summary...") 99 | } 100 | } 101 | } 102 | 103 | export { 104 | cliScannerURL, 105 | defaultSecureEndpoint, 106 | pullScanner, 107 | cliScannerName, 108 | executeScan, 109 | cliScannerResult, 110 | }; 111 | 112 | if (require.main === module) { 113 | run(); 114 | } 115 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} **/ 2 | module.exports = { 3 | testEnvironment: "node", 4 | transform: { 5 | "^.+.tsx?$": ["ts-jest",{}], 6 | }, 7 | "testPathIgnorePatterns": ["build"], 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-inline-scan-action", 3 | "version": "5.2.0", 4 | "description": "This actions performs image analysis on locally built container image and posts the result of the analysis to Sysdig Secure.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint . --ignore-pattern 'build/*'", 8 | "build": "tsc", 9 | "prepare": "npm run build && ncc build build/index.js -o dist --source-map --license licenses.txt", 10 | "test": "jest", 11 | "all": "npm run lint && npm run prepare && npm run test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sysdiglabs/secure-inline-scan-action.git" 16 | }, 17 | "keywords": [ 18 | "sysdig", 19 | "secure", 20 | "container", 21 | "image", 22 | "scanning", 23 | "docker" 24 | ], 25 | "author": "airadier", 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/sysdiglabs/secure-inline-scan-action/issues" 29 | }, 30 | "homepage": "https://github.com/sysdiglabs/secure-inline-scan-action#readme", 31 | "dependencies": { 32 | "@actions/core": "^1.10.1", 33 | "@actions/exec": "^1.1.0", 34 | "@actions/github": "^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^29.5.12", 38 | "@types/tmp": "^0.2.6", 39 | "@vercel/ncc": "^0.36.1", 40 | "eslint": "^7.32.0", 41 | "jest": "^29.7.0", 42 | "tmp": "^0.2.1", 43 | "ts-jest": "^29.2.3", 44 | "typescript": "^5.5.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { cliScannerResult, cliScannerURL, ComposeFlags, ScanMode, scannerURLForVersion } from './scanner'; 3 | import { Severity, SeverityNames } from './report'; 4 | 5 | export const defaultSecureEndpoint = "https://secure.sysdig.com/" 6 | 7 | interface ActionInputParameters { 8 | cliScannerURL: string; 9 | cliScannerVersion: string; 10 | registryUser: string; 11 | registryPassword: string; 12 | stopOnFailedPolicyEval: boolean; 13 | stopOnProcessingError: boolean; 14 | standalone: boolean; 15 | dbPath: string; 16 | skipUpload: boolean; 17 | skipSummary: boolean; 18 | usePolicies: string; 19 | overridePullString: string; 20 | imageTag: string; 21 | sysdigSecureToken: string; 22 | sysdigSecureURL: string; 23 | sysdigSkipTLS: boolean; 24 | severityAtLeast?: string; 25 | packageTypes?: string; 26 | notPackageTypes?: string; 27 | excludeAccepted?: boolean; 28 | groupByPackage: boolean; 29 | extraParameters: string; 30 | mode: ScanMode; 31 | recursive: boolean; 32 | minimumSeverity: string; 33 | iacScanPath: string; 34 | } 35 | 36 | export class ActionInputs { 37 | private readonly _params: ActionInputParameters; 38 | public get params(): ActionInputParameters { 39 | return this._params; 40 | } 41 | private constructor(params: ActionInputParameters) { 42 | ActionInputs.validateInputs(params); 43 | this._params = params; 44 | } 45 | 46 | static from(any: any): ActionInputs { 47 | return new ActionInputs(any as ActionInputParameters); 48 | } 49 | 50 | static fromJSON(jsonContents: string): ActionInputs { 51 | return ActionInputs.from(JSON.parse(jsonContents)) 52 | } 53 | 54 | static parseActionInputs(): ActionInputs { 55 | return ActionInputs.overridingParsedActionInputs({}); 56 | } 57 | 58 | static overridingParsedActionInputs(overrides: { [key: string]: any }) { 59 | 60 | const params: ActionInputParameters = { 61 | cliScannerURL: core.getInput('cli-scanner-url') || cliScannerURL, 62 | cliScannerVersion: core.getInput('cli-scanner-version'), 63 | registryUser: core.getInput('registry-user'), 64 | registryPassword: core.getInput('registry-password'), 65 | stopOnFailedPolicyEval: core.getInput('stop-on-failed-policy-eval') == 'true', 66 | stopOnProcessingError: core.getInput('stop-on-processing-error') == 'true', 67 | standalone: core.getInput('standalone') == 'true', 68 | dbPath: core.getInput('db-path'), 69 | skipUpload: core.getInput('skip-upload') == 'true', 70 | skipSummary: core.getInput('skip-summary') == 'true', 71 | usePolicies: core.getInput('use-policies'), 72 | overridePullString: core.getInput('override-pullstring'), 73 | imageTag: core.getInput('image-tag'), 74 | sysdigSecureToken: core.getInput('sysdig-secure-token'), 75 | sysdigSecureURL: core.getInput('sysdig-secure-url') || defaultSecureEndpoint, 76 | sysdigSkipTLS: core.getInput('sysdig-skip-tls') == 'true', 77 | severityAtLeast: core.getInput('severity-at-least') || undefined, 78 | packageTypes: core.getInput('package-types') || undefined, 79 | notPackageTypes: core.getInput('not-package-types') || undefined, 80 | excludeAccepted: core.getInput('exclude-accepted') === 'true', 81 | groupByPackage: core.getInput('group-by-package') == 'true', 82 | extraParameters: core.getInput('extra-parameters'), 83 | mode: ScanMode.fromString(core.getInput('mode')) || ScanMode.vm, 84 | recursive: core.getInput('recursive') == 'true', 85 | minimumSeverity: core.getInput('minimum-severity'), 86 | iacScanPath: core.getInput('iac-scan-path') || './', 87 | }; 88 | 89 | const overridenParams = { 90 | ...params, 91 | ...overrides, 92 | }; 93 | 94 | 95 | return ActionInputs.from(overridenParams); 96 | } 97 | 98 | get cliScannerURL(): string { 99 | return this.params.cliScannerURL 100 | } 101 | 102 | get mode() { 103 | return this.params.mode; 104 | } 105 | 106 | get stopOnProcessingError() { 107 | return this.params.stopOnProcessingError 108 | } 109 | 110 | get standalone() { 111 | return this.params.standalone 112 | } 113 | 114 | get stopOnFailedPolicyEval() { 115 | return this.params.stopOnFailedPolicyEval 116 | } 117 | 118 | get skipSummary() { 119 | return this.params.skipSummary 120 | } 121 | 122 | get groupByPackage(): boolean { 123 | return this.params.groupByPackage 124 | } 125 | 126 | get severityAtLeast() { 127 | return this.params.severityAtLeast 128 | } 129 | 130 | get packageTypes() { 131 | return this.params.packageTypes; 132 | } 133 | get notPackageTypes() { 134 | return this.params.notPackageTypes; 135 | } 136 | get excludeAccepted() { 137 | return this.params.excludeAccepted; 138 | } 139 | 140 | get imageTag() { 141 | return this.params.imageTag 142 | } 143 | 144 | get overridePullString() { 145 | return this.params.overridePullString 146 | } 147 | 148 | private static validateInputs(params: ActionInputParameters) { 149 | if (!params.standalone && !params.sysdigSecureToken) { 150 | core.setFailed("Sysdig Secure Token is required for standard execution, please set your token or remove the standalone input."); 151 | throw new Error("Sysdig Secure Token is required for standard execution, please set your token or remove the standalone input."); 152 | } 153 | 154 | if (params.mode && params.mode == ScanMode.vm && !params.imageTag) { 155 | core.setFailed("image-tag is required for VM mode."); 156 | throw new Error("image-tag is required for VM mode."); 157 | } 158 | 159 | if (params.mode && params.mode == ScanMode.iac && params.iacScanPath == "") { 160 | core.setFailed("iac-scan-path can't be empty, please specify the path you want to scan your manifest resources."); 161 | throw new Error("iac-scan-path can't be empty, please specify the path you want to scan your manifest resources."); 162 | } 163 | 164 | if (params.severityAtLeast && params.severityAtLeast != "any" && !SeverityNames.includes(params.severityAtLeast.toLowerCase() as Severity)) { 165 | core.setFailed(`Invalid severity-at-least value "${params.severityAtLeast}". Allowed values: any, critical, high, medium, low, negligible.`); 166 | throw new Error(`Invalid severity-at-least value "${params.severityAtLeast}". Allowed values: any, critical, high, medium, low, negligible.`); 167 | } 168 | } 169 | 170 | // FIXME(fede) this also modifies the opts.cliScannerURL, which is something we don't want 171 | public composeFlags(): ComposeFlags { 172 | if (this.params.cliScannerVersion && this.params.cliScannerURL == cliScannerURL) { 173 | this.params.cliScannerURL = scannerURLForVersion(this.params.cliScannerVersion) 174 | } 175 | 176 | let envvars: { [key: string]: string } = {} 177 | envvars['SECURE_API_TOKEN'] = this.params.sysdigSecureToken || ""; 178 | 179 | let flags = "" 180 | 181 | if (this.params.registryUser) { 182 | envvars['REGISTRY_USER'] = this.params.registryUser; 183 | } 184 | 185 | if (this.params.registryPassword) { 186 | envvars['REGISTRY_PASSWORD'] = this.params.registryPassword; 187 | } 188 | 189 | if (this.params.standalone) { 190 | flags += " --standalone"; 191 | } 192 | 193 | if (this.params.sysdigSecureURL) { 194 | flags += ` --apiurl ${this.params.sysdigSecureURL}`; 195 | } 196 | 197 | if (this.params.dbPath) { 198 | flags += ` --dbpath=${this.params.dbPath}`; 199 | } 200 | 201 | if (this.params.skipUpload) { 202 | flags += ' --skipupload'; 203 | } 204 | 205 | if (this.params.usePolicies) { 206 | flags += ` --policy=${this.params.usePolicies}`; 207 | } 208 | 209 | if (this.params.sysdigSkipTLS) { 210 | flags += ` --skiptlsverify`; 211 | } 212 | 213 | if (this.params.overridePullString) { 214 | flags += ` --override-pullstring=${this.params.overridePullString}`; 215 | } 216 | 217 | if (this.params.extraParameters) { 218 | flags += ` ${this.params.extraParameters}`; 219 | } 220 | 221 | if (this.params.mode == ScanMode.iac) { 222 | flags += ` --iac`; 223 | 224 | if (this.params.recursive) { 225 | flags += ` -r`; 226 | } 227 | if (this.params.minimumSeverity) { 228 | flags += ` -f=${this.params.minimumSeverity}`; 229 | } 230 | 231 | flags += ` ${this.params.iacScanPath}`; 232 | } 233 | 234 | if (this.params.mode == ScanMode.vm) { 235 | flags += ` --json-scan-result=${cliScannerResult}` 236 | flags += ` ${this.params.imageTag}`; 237 | } 238 | 239 | return { 240 | envvars: envvars, 241 | flags: flags 242 | } 243 | } 244 | 245 | public printOptions() { 246 | if (this.params.standalone) { 247 | core.info(`[!] Running in Standalone Mode.`); 248 | } 249 | 250 | if (this.params.sysdigSecureURL) { 251 | core.info('Sysdig Secure URL: ' + this.params.sysdigSecureURL); 252 | } 253 | 254 | if (this.params.registryUser && this.params.registryPassword) { 255 | core.info(`Using specified Registry credentials.`); 256 | } 257 | 258 | core.info(`Stop on Failed Policy Evaluation: ${this.params.stopOnFailedPolicyEval}`); 259 | 260 | core.info(`Stop on Processing Error: ${this.params.stopOnProcessingError}`); 261 | 262 | if (this.params.skipUpload) { 263 | core.info(`Skipping scan results upload to Sysdig Secure...`); 264 | } 265 | 266 | if (this.params.dbPath) { 267 | core.info(`DB Path: ${this.params.dbPath}`); 268 | } 269 | 270 | core.info(`Sysdig skip TLS: ${this.params.sysdigSkipTLS}`); 271 | 272 | if (this.params.severityAtLeast) { 273 | core.info(`Severity level: ${this.params.severityAtLeast}`); 274 | } 275 | 276 | if (this.params.packageTypes) { 277 | core.info(`Package types included: ${this.params.packageTypes}`); 278 | } 279 | 280 | if (this.params.notPackageTypes) { 281 | core.info(`Package types excluded: ${this.params.notPackageTypes}`); 282 | } 283 | 284 | if (this.params.excludeAccepted !== undefined) { 285 | core.info(`Exclude vulnerabilities with accepted risks: ${this.params.excludeAccepted}`); 286 | } 287 | 288 | core.info('Analyzing image: ' + this.params.imageTag); 289 | 290 | if (this.params.overridePullString) { 291 | core.info(` * Image PullString will be overwritten as ${this.params.overridePullString}`); 292 | } 293 | 294 | if (this.params.skipSummary) { 295 | core.info("This run will NOT generate a SUMMARY."); 296 | } 297 | } 298 | } 299 | 300 | -------------------------------------------------------------------------------- /src/report.ts: -------------------------------------------------------------------------------- 1 | export interface Report { 2 | info: Info 3 | scanner: Scanner 4 | result: Result 5 | } 6 | 7 | export interface Info { 8 | scanTime: string 9 | scanDuration: string 10 | resultUrl: string 11 | resultId: string 12 | } 13 | 14 | export interface Scanner { 15 | name: string 16 | version: string 17 | } 18 | 19 | export interface Result { 20 | type: string 21 | metadata: Metadata 22 | vulnTotalBySeverity: VulnTotalBySeverity 23 | fixableVulnTotalBySeverity: FixableVulnTotalBySeverity 24 | exploitsCount: number 25 | packages: Package[] 26 | layers?: Layer[] 27 | policyEvaluations: PolicyEvaluation[] 28 | policyEvaluationsResult: string 29 | riskAcceptanceDefinitions?: RiskAcceptanceDefinition[] 30 | } 31 | 32 | export interface Metadata { 33 | pullString: string 34 | imageId: string 35 | digest: string 36 | baseOs: string 37 | size: number 38 | os: string 39 | architecture: string 40 | labels?: { [key: string]: string } 41 | layersCount: number 42 | createdAt: string 43 | } 44 | 45 | export interface VulnTotalBySeverity { 46 | critical: number 47 | high: number 48 | low: number 49 | medium: number 50 | negligible: number 51 | } 52 | 53 | export interface FixableVulnTotalBySeverity { 54 | critical: number 55 | high: number 56 | low: number 57 | medium: number 58 | negligible: number 59 | } 60 | 61 | export interface Package { 62 | type: string 63 | name: string 64 | version: string 65 | path: string 66 | layerDigest?: string 67 | suggestedFix?: string 68 | vulns?: Vuln[] 69 | } 70 | 71 | export interface Vuln { 72 | name: string 73 | severity: SarifSeverity 74 | cvssScore: CvssScore 75 | disclosureDate: string 76 | solutionDate?: string 77 | exploitable: boolean 78 | fixedInVersion?: string 79 | publishDateByVendor: PublishDateByVendor 80 | annotations?: { [key: string]: string } 81 | acceptedRisks?: AcceptedRisk[] 82 | } 83 | 84 | export interface FilterOptions { 85 | minSeverity?: Severity; 86 | packageTypes?: string[]; 87 | notPackageTypes?: string[]; 88 | excludeAccepted?: boolean; 89 | } 90 | 91 | 92 | export interface SarifSeverity { 93 | value: string 94 | sourceName: string 95 | } 96 | 97 | export const SeverityNames = ["critical", "high", "medium", "low", "negligible"] as const; 98 | export type Severity = typeof SeverityNames[number]; 99 | 100 | export interface CvssScore { 101 | value: Value 102 | sourceName: string 103 | } 104 | 105 | export interface Value { 106 | version: string 107 | score: number 108 | vector: string 109 | } 110 | 111 | export interface PublishDateByVendor { 112 | nvd?: string 113 | vulndb: string 114 | cisakev?: string 115 | } 116 | 117 | export interface AcceptedRisk { 118 | index: number 119 | ref: string 120 | id: string 121 | } 122 | 123 | export interface Layer { 124 | digest?: string 125 | size?: number 126 | command: string 127 | vulns: Vulns 128 | runningVulns: RunningVulns 129 | baseImages: BaseImage[] 130 | } 131 | 132 | export interface Vulns { 133 | critical?: number 134 | high?: number 135 | low?: number 136 | medium?: number 137 | negligible?: number 138 | } 139 | 140 | export interface RunningVulns { } 141 | 142 | export interface BaseImage { 143 | pullstrings: string[] 144 | } 145 | 146 | export interface PolicyEvaluation { 147 | name: string 148 | identifier: string 149 | type: string 150 | bundles: Bundle[] 151 | acceptedRiskTotal: number 152 | evaluationResult: string 153 | createdAt: string 154 | updatedAt: string 155 | } 156 | 157 | export interface Bundle { 158 | name: string 159 | identifier: string 160 | type: string 161 | rules: Rule[] 162 | createdAt: string 163 | updatedAt: string 164 | } 165 | 166 | export interface Rule { 167 | ruleType: string 168 | failureType: string 169 | description: string 170 | failures?: Failure[] 171 | evaluationResult: string 172 | predicates: Predicate[] 173 | } 174 | 175 | export interface Failure { 176 | pkgIndex?: number 177 | vulnInPkgIndex?: number 178 | ref?: string 179 | description?: string 180 | remediation?: string 181 | Arguments?: Arguments 182 | acceptedRisks?: AcceptedRisk[] 183 | } 184 | 185 | export interface Arguments { 186 | instructions?: string[] 187 | } 188 | 189 | export interface Predicate { 190 | type: string 191 | extra?: Extra 192 | } 193 | 194 | export interface Extra { 195 | level?: string 196 | age?: number 197 | vulnIds?: string[] 198 | } 199 | 200 | export interface RiskAcceptanceDefinition { 201 | id: string 202 | entityType: string 203 | entityValue: string 204 | context: any[] 205 | status: string 206 | reason: string 207 | description: string 208 | expirationDate: string 209 | createdAt: string 210 | updatedAt: string 211 | } 212 | 213 | const severityOrder = ["negligible", "low", "medium", "high", "critical"]; 214 | 215 | export function isSeverityGte(a: string, b: string): boolean { 216 | return severityOrder.indexOf(a.toLocaleLowerCase()) >= severityOrder.indexOf(b.toLocaleLowerCase()); 217 | } 218 | 219 | export function filterPackages(pkgs: Package[], filters: FilterOptions): Package[] { 220 | if (!Array.isArray(pkgs)) return []; 221 | return pkgs 222 | .filter(pkg => { 223 | const pkgType = pkg.type?.toLowerCase(); 224 | if (filters.packageTypes && filters.packageTypes.length > 0 && 225 | !filters.packageTypes.map(t => t.toLowerCase()).includes(pkgType)) return false; 226 | if (filters.notPackageTypes && filters.notPackageTypes.length > 0 && 227 | filters.notPackageTypes.map(t => t.toLowerCase()).includes(pkgType)) return false; 228 | return true; 229 | }) 230 | .map(pkg => { 231 | let vulns = pkg.vulns?.filter(vuln => { 232 | if (filters.minSeverity && !isSeverityGte(vuln.severity.value, filters.minSeverity)) return false; 233 | if (filters.excludeAccepted && Array.isArray(vuln.acceptedRisks) && vuln.acceptedRisks.length > 0) return false; 234 | return true; 235 | }) || []; 236 | return { ...pkg, vulns }; 237 | }) 238 | .filter(pkg => pkg.vulns && pkg.vulns.length > 0); 239 | } 240 | -------------------------------------------------------------------------------- /src/sarif.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import fs from 'fs'; 3 | import { Package, Report, FilterOptions, Vuln, filterPackages, SeverityNames } from './report'; 4 | 5 | import { version } from '../package.json'; 6 | const toolVersion = `${version}`; 7 | const dottedQuadToolVersion = `${version}.0`; 8 | 9 | interface SARIFResult { 10 | ruleId: string; 11 | level: string; 12 | message: { 13 | text: string; 14 | }; 15 | locations: { 16 | physicalLocation: { 17 | artifactLocation: { 18 | uri: string; 19 | uriBaseId: string; 20 | }; 21 | }; 22 | message: { 23 | text: string; 24 | }; 25 | }[]; 26 | } 27 | 28 | interface SARIFRule { 29 | id: string; 30 | name: string; 31 | shortDescription: { 32 | text: string; 33 | }; 34 | fullDescription: { 35 | text: string; 36 | }; 37 | helpUri: string; 38 | help: { 39 | text: string; 40 | markdown: string; 41 | }; 42 | properties: { 43 | precision: string; 44 | 'security-severity': string; 45 | tags: string[]; 46 | }; 47 | } 48 | 49 | export function generateSARIFReport(data: Report, groupByPackage: boolean, filters?: FilterOptions) { 50 | let sarifOutput = vulnerabilities2SARIF(data, groupByPackage, filters); 51 | core.setOutput("sarifReport", "./sarif.json"); 52 | fs.writeFileSync("./sarif.json", JSON.stringify(sarifOutput, null, 2)); 53 | } 54 | 55 | export function vulnerabilities2SARIF( 56 | data: Report, 57 | groupByPackage: boolean, 58 | filters?: FilterOptions 59 | ) { 60 | 61 | const filteredPackages = filterPackages(data.result.packages, filters ?? {}); 62 | const filteredData = { ...data, result: { ...data.result, packages: filteredPackages } }; 63 | 64 | let rules: SARIFRule[] = []; 65 | let results: SARIFResult[] = []; 66 | 67 | if (groupByPackage) { 68 | [rules, results] = vulnerabilities2SARIFResByPackage(filteredData) 69 | } else { 70 | [rules, results] = vulnerabilities2SARIFRes(filteredData) 71 | } 72 | 73 | const runs = [{ 74 | tool: { 75 | driver: { 76 | name: "sysdig-cli-scanner", 77 | fullName: "Sysdig Vulnerability CLI Scanner", 78 | informationUri: "https://docs.sysdig.com/en/docs/installation/sysdig-secure/install-vulnerability-cli-scanner", 79 | version: toolVersion, 80 | semanticVersion: toolVersion, 81 | dottedQuadFileVersion: dottedQuadToolVersion, 82 | rules: rules 83 | } 84 | }, 85 | logicalLocations: [ 86 | { 87 | name: "container-image", 88 | fullyQualifiedName: "container-image", 89 | kind: "namespace" 90 | } 91 | ], 92 | results: results, 93 | columnKind: "utf16CodeUnits", 94 | properties: { 95 | pullString: data.result.metadata.pullString, 96 | digest: data.result.metadata.digest, 97 | imageId: data.result.metadata.imageId, 98 | architecture: data.result.metadata.architecture, 99 | baseOs: data.result.metadata.baseOs, 100 | os: data.result.metadata.os, 101 | size: data.result.metadata.size, 102 | layersCount: data.result.metadata.layersCount, 103 | resultUrl: data.info.resultUrl || "", 104 | resultId: data.info.resultId || "", 105 | } 106 | }]; 107 | 108 | 109 | const sarifOutput = { 110 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 111 | version: "2.1.0", 112 | runs: runs 113 | }; 114 | 115 | return (sarifOutput); 116 | } 117 | 118 | function numericPriorityForSeverity(severity: string): number { 119 | let sevNum = SeverityNames.indexOf(severity.toLowerCase() as any); 120 | sevNum = sevNum === -1 ? 5 : sevNum; 121 | return sevNum; 122 | } 123 | 124 | function vulnerabilities2SARIFResByPackage(data: Report): [SARIFRule[], SARIFResult[]] { 125 | let rules: SARIFRule[] = []; 126 | let results: SARIFResult[] = []; 127 | let resultUrl = ""; 128 | let baseUrl: string | undefined; 129 | 130 | if (data.info && data.result) { 131 | if (data.info.resultUrl) { 132 | resultUrl = data.info.resultUrl; 133 | baseUrl = resultUrl.slice(0, resultUrl.lastIndexOf('/')); 134 | } 135 | 136 | data.result.packages.forEach(pkg => { 137 | if (!pkg.vulns) { 138 | return 139 | } 140 | 141 | let helpUri = ""; 142 | let fullDescription = ""; 143 | let severityLevel = ""; 144 | let minSeverityNum = 5; 145 | let score = 0.0; 146 | pkg.vulns.forEach(vuln => { 147 | fullDescription += `${getSARIFVulnFullDescription(pkg, vuln)}\n\n\n`; 148 | 149 | const sevNum = numericPriorityForSeverity(vuln.severity.value); 150 | 151 | if (sevNum < minSeverityNum) { 152 | severityLevel = vuln.severity.value.toLowerCase(); 153 | minSeverityNum = sevNum; 154 | } 155 | 156 | if (vuln.cvssScore.value.score > score) { 157 | score = vuln.cvssScore.value.score; 158 | } 159 | }); 160 | if (baseUrl) helpUri = `${baseUrl}/content?filter=freeText+in+("${pkg.name}")`; 161 | 162 | 163 | let rule: SARIFRule = { 164 | id: pkg.name, 165 | name: pkg.name, 166 | shortDescription: { 167 | text: `Vulnerable package: ${pkg.name}` 168 | }, 169 | fullDescription: { 170 | text: fullDescription 171 | }, 172 | helpUri: helpUri, 173 | help: getSARIFPkgHelp(pkg), 174 | properties: { 175 | precision: "very-high", 176 | 'security-severity': `${score}`, 177 | tags: [ 178 | 'vulnerability', 179 | 'security', 180 | severityLevel 181 | ] 182 | } 183 | } 184 | rules.push(rule); 185 | 186 | let result: SARIFResult = { 187 | ruleId: pkg.name, 188 | level: check_level(severityLevel), 189 | message: { 190 | text: getSARIFReportMessageByPackage(data, pkg, baseUrl) 191 | }, 192 | locations: [ 193 | { 194 | physicalLocation: { 195 | artifactLocation: { 196 | uri: `file:///${sanitizeImageName(data.result.metadata.pullString)}`, 197 | uriBaseId: "ROOTPATH" 198 | } 199 | }, 200 | message: { 201 | text: `${data.result.metadata.pullString} - ${pkg.name}@${pkg.version}` 202 | } 203 | } 204 | ] 205 | } 206 | results.push(result) 207 | }); 208 | } 209 | 210 | return [rules, results]; 211 | } 212 | 213 | function sanitizeImageName(imageName: string) { 214 | // Replace / and : with - 215 | return imageName.replace(/[\/:]/g, '-'); 216 | } 217 | 218 | function vulnerabilities2SARIFRes(data: Report): [SARIFRule[], SARIFResult[]] { 219 | let results: SARIFResult[] = []; 220 | let rules: SARIFRule[] = []; 221 | let ruleIds: string[] = []; 222 | let resultUrl = ""; 223 | let baseUrl: string | undefined; 224 | 225 | if (data.info && data.result) { 226 | if (data.info.resultUrl) { 227 | resultUrl = data.info.resultUrl; 228 | baseUrl = resultUrl.slice(0, resultUrl.lastIndexOf('/')); 229 | } 230 | 231 | data.result.packages.forEach(pkg => { 232 | if (!pkg.vulns) { 233 | return 234 | } 235 | 236 | pkg.vulns.forEach(vuln => { 237 | if (!(vuln.name in ruleIds)) { 238 | ruleIds.push(vuln.name) 239 | let rule = { 240 | id: vuln.name, 241 | name: pkg.type, 242 | shortDescription: { 243 | text: getSARIFVulnShortDescription(pkg, vuln) 244 | }, 245 | fullDescription: { 246 | text: getSARIFVulnFullDescription(pkg, vuln) 247 | }, 248 | helpUri: `https://nvd.nist.gov/vuln/detail/${vuln.name}`, 249 | help: getSARIFVulnHelp(pkg, vuln), 250 | properties: { 251 | precision: "very-high", 252 | 'security-severity': `${vuln.cvssScore.value.score}`, 253 | tags: [ 254 | 'vulnerability', 255 | 'security', 256 | vuln.severity.value 257 | ] 258 | } 259 | } 260 | rules.push(rule) 261 | } 262 | 263 | let result = { 264 | ruleId: vuln.name, 265 | level: check_level(vuln.severity.value), 266 | message: { 267 | text: getSARIFReportMessage(data, vuln, pkg, baseUrl) 268 | }, 269 | locations: [ 270 | { 271 | physicalLocation: { 272 | artifactLocation: { 273 | uri: `file:///${sanitizeImageName(data.result.metadata.pullString)}`, 274 | uriBaseId: "ROOTPATH" 275 | } 276 | }, 277 | message: { 278 | text: `${data.result.metadata.pullString} - ${pkg.name}@${pkg.version}` 279 | } 280 | } 281 | ] 282 | } 283 | results.push(result) 284 | }); 285 | }); 286 | } 287 | 288 | return [rules, results]; 289 | } 290 | function getSARIFVulnShortDescription(pkg: Package, vuln: Vuln) { 291 | return `${vuln.name} Severity: ${vuln.severity.value} Package: ${pkg.name}`; 292 | } 293 | 294 | function getSARIFVulnFullDescription(pkg: Package, vuln: Vuln) { 295 | return `${vuln.name} 296 | Severity: ${vuln.severity.value} 297 | Package: ${pkg.name} 298 | Type: ${pkg.type} 299 | Fix: ${pkg.suggestedFix || "No fix available"} 300 | URL: https://nvd.nist.gov/vuln/detail/${vuln.name}`; 301 | } 302 | 303 | function getSARIFPkgHelp(pkg: Package) { 304 | let text = ""; 305 | pkg.vulns?.forEach(vuln => { 306 | text += `Vulnerability ${vuln.name} 307 | Severity: ${vuln.severity.value} 308 | Package: ${pkg.name} 309 | CVSS Score: ${vuln.cvssScore.value.score} 310 | CVSS Version: ${vuln.cvssScore.value.version} 311 | CVSS Vector: ${vuln.cvssScore.value.vector} 312 | Version: ${pkg.version} 313 | Fix Version: ${pkg.suggestedFix || "No fix available"} 314 | Exploitable: ${vuln.exploitable} 315 | Type: ${pkg.type} 316 | Location: ${pkg.path} 317 | URL: https://nvd.nist.gov/vuln/detail/${vuln.name}\n\n\n` 318 | }); 319 | 320 | let markdown = `| Vulnerability | Severity | CVSS Score | CVSS Version | CVSS Vector | Exploitable | 321 | | -------- | ------- | ---------- | ------------ | ----------- | ----------- |\n`; 322 | 323 | pkg.vulns?.forEach(vuln => { markdown += `| ${vuln.name} | ${vuln.severity.value} | ${vuln.cvssScore.value.score} | ${vuln.cvssScore.value.version} | ${vuln.cvssScore.value.vector} | ${vuln.exploitable} |\n` }); 324 | 325 | return { 326 | text: text, 327 | markdown: markdown 328 | }; 329 | } 330 | 331 | function getSARIFVulnHelp(pkg: Package, vuln: Vuln) { 332 | return { 333 | text: `Vulnerability ${vuln.name} 334 | Severity: ${vuln.severity.value} 335 | Package: ${pkg.name} 336 | CVSS Score: ${vuln.cvssScore.value.score} 337 | CVSS Version: ${vuln.cvssScore.value.version} 338 | CVSS Vector: ${vuln.cvssScore.value.vector} 339 | Version: ${pkg.version} 340 | Fix Version: ${pkg.suggestedFix || "No fix available"} 341 | Exploitable: ${vuln.exploitable} 342 | Type: ${pkg.type} 343 | Location: ${pkg.path} 344 | URL: https://nvd.nist.gov/vuln/detail/${vuln.name}`, 345 | markdown: ` 346 | **Vulnerability [${vuln.name}](https://nvd.nist.gov/vuln/detail/${vuln.name})** 347 | | Severity | Package | CVSS Score | CVSS Version | CVSS Vector | Fixed Version | Exploitable | 348 | | -------- | ------- | ---------- | ------------ | ----------- | ------------- | ----------- | 349 | | ${vuln.severity.value} | ${pkg.name} | ${vuln.cvssScore.value.score} | ${vuln.cvssScore.value.version} | ${vuln.cvssScore.value.vector} | ${pkg.suggestedFix || "None"} | ${vuln.exploitable} |` 350 | } 351 | } 352 | function getSARIFReportMessageByPackage(data: Report, pkg: Package, baseUrl?: string) { 353 | let message = `Full image scan results in Sysdig UI: [${data.result.metadata.pullString} scan result](${data.info.resultUrl})\n`; 354 | 355 | if (baseUrl) { 356 | message += `Package: [${pkg.name}](${baseUrl}/content?filter=freeText+in+("${pkg.name}"))\n`; 357 | } else { 358 | message += `Package: ${pkg.name}\n`; 359 | } 360 | 361 | message += `Package type: ${pkg.type} 362 | Installed Version: ${pkg.version} 363 | Package path: ${pkg.path}\n`; 364 | 365 | pkg.vulns?.forEach(vuln => { 366 | message += `.\n`; 367 | 368 | if (baseUrl) { 369 | message += `Vulnerability: [${vuln.name}](${baseUrl}/vulnerabilities?filter=freeText+in+("${vuln.name}"))\n`; 370 | } else { 371 | message += `Vulnerability: ${vuln.name}\n`; 372 | } 373 | 374 | message += `Severity: ${vuln.severity.value} 375 | CVSS Score: ${vuln.cvssScore.value.score} 376 | CVSS Version: ${vuln.cvssScore.value.version} 377 | CVSS Vector: ${vuln.cvssScore.value.vector} 378 | Fixed Version: ${(vuln.fixedInVersion || 'No fix available')} 379 | Exploitable: ${vuln.exploitable} 380 | Link to NVD: [${vuln.name}](https://nvd.nist.gov/vuln/detail/${vuln.name})\n`; 381 | }); 382 | 383 | 384 | return message; 385 | } 386 | 387 | function getSARIFReportMessage(data: Report, vuln: Vuln, pkg: Package, baseUrl: string | undefined) { 388 | let message = `Full image scan results in Sysdig UI: [${data.result.metadata.pullString} scan result](${data.info.resultUrl})\n`; 389 | 390 | if (baseUrl) { 391 | message += `Package: [${pkg.name}](${baseUrl}/content?filter=freeText+in+("${pkg.name}"))\n`; 392 | } else { 393 | message += `Package: ${pkg.name}\n`; 394 | } 395 | 396 | message += `Package type: ${pkg.type} 397 | Installed Version: ${pkg.version} 398 | Package path: ${pkg.path}\n`; 399 | 400 | if (baseUrl) { 401 | message += `Vulnerability: [${vuln.name}](${baseUrl}/vulnerabilities?filter=freeText+in+("${vuln.name}"))\n`; 402 | } else { 403 | message += `Vulnerability: ${vuln.name}\n`; 404 | } 405 | message += `Severity: ${vuln.severity.value} 406 | CVSS Score: ${vuln.cvssScore.value.score} 407 | CVSS Version: ${vuln.cvssScore.value.version} 408 | CVSS Vector: ${vuln.cvssScore.value.vector} 409 | Fixed Version: ${(vuln.fixedInVersion || 'No fix available')} 410 | Exploitable: ${vuln.exploitable} 411 | Link to NVD: [${vuln.name}](https://nvd.nist.gov/vuln/detail/${vuln.name})`; 412 | 413 | return message; 414 | } 415 | 416 | // Sysdig to SARIF severity convertion 417 | const LEVELS: any = { 418 | "error": ["High", "Critical"], 419 | "warning": ["Medium"], 420 | "note": ["Negligible", "Low"] 421 | } 422 | 423 | function check_level(sev_value: string) { 424 | let level = "note"; 425 | 426 | for (let key in LEVELS) { 427 | if (sev_value in LEVELS[key]) { 428 | level = key 429 | } 430 | } 431 | 432 | return level 433 | } 434 | -------------------------------------------------------------------------------- /src/scanner.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as exec from '@actions/exec'; 3 | import os from 'os'; 4 | import process from 'process'; 5 | const performance = require('perf_hooks').performance; 6 | 7 | const cliScannerVersion = "1.22.1" 8 | const cliScannerOS = getRunOS() 9 | const cliScannerArch = getRunArch() 10 | const cliScannerURLBase = "https://download.sysdig.com/scanning/bin/sysdig-cli-scanner"; 11 | export const cliScannerName = "sysdig-cli-scanner" 12 | export const cliScannerResult = "scan-result.json" 13 | export const cliScannerURL = `${cliScannerURLBase}/${cliScannerVersion}/${cliScannerOS}/${cliScannerArch}/${cliScannerName}` 14 | 15 | export enum ScanMode { 16 | vm = "vm", 17 | iac = "iac", 18 | } 19 | 20 | export namespace ScanMode { 21 | export function fromString(str: string): ScanMode | undefined { 22 | switch (str.toLowerCase()) { 23 | case "vm": 24 | return ScanMode.vm; 25 | case "iac": 26 | return ScanMode.iac; 27 | } 28 | } 29 | } 30 | 31 | export async function pullScanner(scannerURL: string) { 32 | let start = performance.now(); 33 | core.info('Pulling cli-scanner from: ' + scannerURL); 34 | let cmd = `wget ${scannerURL} -O ./${cliScannerName}`; 35 | let retCode = await exec.exec(cmd, undefined, { silent: true }); 36 | 37 | if (retCode == 0) { 38 | cmd = `chmod u+x ./${cliScannerName}`; 39 | await exec.exec(cmd, undefined, { silent: true }); 40 | } else { 41 | core.error(`Falied to pull scanner using "${scannerURL}"`) 42 | } 43 | 44 | core.info("Scanner pull took " + Math.round(performance.now() - start) + " milliseconds."); 45 | return retCode; 46 | } 47 | 48 | export interface ScanExecutionResult { 49 | ReturnCode: number; 50 | Output: string; 51 | Error: string; 52 | } 53 | 54 | 55 | // If custom scanner version is specified 56 | export async function executeScan(scanFlags: ComposeFlags): Promise { 57 | 58 | let { envvars, flags } = scanFlags; 59 | let execOutput = ''; 60 | let errOutput = ''; 61 | 62 | 63 | const scanOptions: exec.ExecOptions = { 64 | env: { 65 | ...Object.fromEntries( 66 | Object.entries(process.env).map(([key, value]) => [key, value ?? ""]) 67 | ), 68 | ...envvars, 69 | }, 70 | silent: true, 71 | ignoreReturnCode: true, 72 | listeners: { 73 | stdout: (data: Buffer) => { 74 | process.stdout.write(data); 75 | }, 76 | stderr: (data: Buffer) => { 77 | process.stderr.write(data); 78 | } 79 | } 80 | }; 81 | 82 | const catOptions: exec.ExecOptions = { 83 | silent: true, 84 | ignoreReturnCode: true, 85 | listeners: { 86 | stdout: (data) => { 87 | execOutput += data.toString(); 88 | }, 89 | stderr: (data) => { 90 | errOutput += data.toString(); 91 | } 92 | } 93 | } 94 | 95 | let start = performance.now(); 96 | let cmd = `./${cliScannerName} ${flags}`; 97 | core.info("Executing: " + cmd); 98 | let retCode = await exec.exec(cmd, undefined, scanOptions); 99 | core.info("Image analysis took " + Math.round(performance.now() - start) + " milliseconds."); 100 | 101 | if (retCode == 0 || retCode == 1) { 102 | cmd = `cat ./${cliScannerResult}`; 103 | await exec.exec(cmd, undefined, catOptions); 104 | } 105 | return { ReturnCode: retCode, Output: execOutput, Error: errOutput }; 106 | } 107 | 108 | export interface ComposeFlags { 109 | envvars: { 110 | [key: string]: string; 111 | }; 112 | flags: string; 113 | } 114 | 115 | export function scannerURLForVersion(version: string): string { 116 | return `${cliScannerURLBase}/${version}/${cliScannerOS}/${cliScannerArch}/${cliScannerName}`; 117 | 118 | } 119 | 120 | function getRunArch() { 121 | let arch = "unknown"; 122 | if (os.arch() == "x64") { 123 | arch = "amd64"; 124 | } else if (os.arch() == "arm64") { 125 | arch = "arm64"; 126 | } 127 | return arch; 128 | } 129 | 130 | function getRunOS() { 131 | let os_name = "unknown"; 132 | if (os.platform() == "linux") { 133 | os_name = "linux"; 134 | } else if (os.platform() == "darwin") { 135 | os_name = "darwin"; 136 | } 137 | return os_name; 138 | } 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/summary.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { FilterOptions, filterPackages, Package, Severity, isSeverityGte, Report, Rule } from "./report"; 3 | import { ActionInputs } from "./action"; 4 | 5 | const EVALUATION: any = { 6 | "failed": "❌", 7 | "passed": "✅" 8 | } 9 | 10 | export async function generateSummary(opts: ActionInputs, data: Report, filters?: FilterOptions) { 11 | const filteredPkgs = filterPackages(data.result.packages, filters || {}); 12 | let filteredData = { ...data, result: { ...data.result, packages: filteredPkgs } }; 13 | 14 | core.summary.emptyBuffer().clear(); 15 | core.summary.addHeading(`Scan Results for ${opts.overridePullString || opts.imageTag}`); 16 | 17 | addVulnTableToSummary(filteredData, filters?.minSeverity); 18 | addVulnsByLayerTableToSummary(filteredData, filters?.minSeverity); 19 | 20 | if (!opts.standalone) { 21 | addReportToSummary(data); 22 | } 23 | 24 | await core.summary.write({ overwrite: true }); 25 | } 26 | 27 | const SEVERITY_ORDER: Severity[] = ["critical", "high", "medium", "low", "negligible"]; 28 | 29 | const SEVERITY_LABELS: Record = { 30 | critical: "🟣 Critical", 31 | high: "🔴 High", 32 | medium: "🟠 Medium", 33 | low: "🟡 Low", 34 | negligible: "⚪ Negligible" 35 | }; 36 | 37 | function countVulnsBySeverity( 38 | packages: Package[], 39 | minSeverity?: Severity 40 | ): { 41 | total: Record; 42 | fixable: Record; 43 | } { 44 | const result = { 45 | total: { critical: 0, high: 0, medium: 0, low: 0, negligible: 0 }, 46 | fixable: { critical: 0, high: 0, medium: 0, low: 0, negligible: 0 } 47 | }; 48 | 49 | for (const pkg of packages) { 50 | for (const vuln of pkg.vulns ?? []) { 51 | const sev = vuln.severity.value.toLowerCase() as Severity; 52 | if (!minSeverity || isSeverityGte(sev, minSeverity)) { 53 | result.total[sev]++; 54 | if (vuln.fixedInVersion || pkg.suggestedFix) { 55 | result.fixable[sev]++; 56 | } 57 | } 58 | } 59 | } 60 | return result; 61 | } 62 | 63 | function addVulnTableToSummary( 64 | data: Report, 65 | minSeverity?: Severity 66 | ) { 67 | const pkgs = data.result.packages; 68 | 69 | const visibleSeverities = SEVERITY_ORDER.filter(sev => 70 | !minSeverity || isSeverityGte(sev, minSeverity) 71 | ); 72 | 73 | const totalVulns = countVulnsBySeverity(pkgs, minSeverity); 74 | 75 | core.summary.addHeading(`Vulnerabilities summary`, 2); 76 | core.summary.addTable([ 77 | [ 78 | { data: '', header: true }, 79 | ...visibleSeverities.map(s => ({ data: SEVERITY_LABELS[s], header: true })) 80 | ], 81 | [ 82 | { data: '⚠️ Total Vulnerabilities', header: true }, 83 | ...visibleSeverities.map(s => `${totalVulns.total[s] ?? 0}`) 84 | ], 85 | [ 86 | { data: '🔧 Fixable Vulnerabilities', header: true }, 87 | ...visibleSeverities.map(s => `${totalVulns.fixable[s] ?? 0}`) 88 | ], 89 | ]); 90 | } 91 | 92 | function addVulnsByLayerTableToSummary(data: Report, minSeverity?: Severity) { 93 | if (!Array.isArray(data.result.layers) || data.result.layers.length === 0) { 94 | return; 95 | } 96 | 97 | const visibleSeverities = SEVERITY_ORDER.filter(sev => 98 | !minSeverity || isSeverityGte(sev, minSeverity) 99 | ); 100 | 101 | core.summary.addHeading(`Package vulnerabilities per layer`, 2); 102 | 103 | let packagesPerLayer: { [key: string]: Package[] } = {}; 104 | data.result.packages.forEach(layerPackage => { 105 | if (layerPackage.layerDigest) { 106 | packagesPerLayer[layerPackage.layerDigest] = (packagesPerLayer[layerPackage.layerDigest] ?? []).concat(layerPackage) 107 | } 108 | }); 109 | 110 | data.result.layers.forEach((layer, index) => { 111 | core.summary.addCodeBlock(`LAYER ${index} - ${layer.command.replace(/\$/g, "$").replace(/\&/g, '&')}`); 112 | if (!layer.digest) { 113 | return; 114 | } 115 | 116 | let packagesWithVulns = (packagesPerLayer[layer.digest] ?? []).filter(pkg => pkg.vulns); 117 | if (packagesWithVulns.length === 0) { 118 | return; 119 | } 120 | 121 | let orderedPackagesBySeverity = packagesWithVulns.sort((a, b) => { 122 | const getSeverityCount = (pkg: Package, severity: string) => 123 | pkg.vulns?.filter((vul: any) => vul.severity.value === severity).length || 0; 124 | 125 | const severities = ['Critical', 'High', 'Medium', 'Low', 'Negligible']; 126 | for (const severity of severities) { 127 | const countA = getSeverityCount(a, severity); 128 | const countB = getSeverityCount(b, severity); 129 | if (countA !== countB) { 130 | return countB - countA; 131 | } 132 | } 133 | return 0; 134 | }); 135 | 136 | core.summary.addTable([ 137 | [ 138 | { data: 'Package', header: true }, 139 | { data: 'Type', header: true }, 140 | { data: 'Version', header: true }, 141 | { data: 'Suggested fix', header: true }, 142 | ...visibleSeverities.map(s => ({ data: SEVERITY_LABELS[s], header: true })), 143 | { data: 'Exploit', header: true }, 144 | ], 145 | ...orderedPackagesBySeverity.map(layerPackage => { 146 | return [ 147 | { data: layerPackage.name }, 148 | { data: layerPackage.type }, 149 | { data: layerPackage.version }, 150 | { data: layerPackage.suggestedFix || "" }, 151 | ...visibleSeverities.map(s => 152 | `${ 153 | layerPackage.vulns?.filter(vuln => vuln.severity.value.toLowerCase() === s).length ?? 0 154 | }` 155 | ), 156 | `${layerPackage.vulns?.filter(vuln => vuln.exploitable).length ?? 0}`, 157 | ]; 158 | }) 159 | ]); 160 | }); 161 | } 162 | 163 | function addReportToSummary(data: Report) { 164 | let policyEvaluations = data.result.policyEvaluations; 165 | let packages = data.result.packages; 166 | 167 | core.summary.addHeading("Policy evaluation summary", 2) 168 | core.summary.addRaw(`Evaluation result: ${data.result.policyEvaluationsResult} ${EVALUATION[data.result.policyEvaluationsResult]}`); 169 | 170 | 171 | let table: { data: string, header?: boolean }[][] = [[ 172 | { data: 'Policy', header: true }, 173 | { data: 'Evaluation', header: true }, 174 | ]]; 175 | 176 | policyEvaluations.forEach(policy => { 177 | table.push([ 178 | { data: `${policy.name}` }, 179 | { data: `${EVALUATION[policy.evaluationResult]}` }, 180 | ]); 181 | }); 182 | 183 | core.summary.addTable(table); 184 | 185 | core.summary.addHeading("Policy failures", 2) 186 | 187 | policyEvaluations.forEach(policy => { 188 | if (policy.evaluationResult != "passed") { 189 | core.summary.addHeading(`Policy: ${policy.name}`, 3) 190 | policy.bundles.forEach(bundle => { 191 | core.summary.addHeading(`Rule Bundle: ${bundle.name}`, 4) 192 | 193 | bundle.rules.forEach(rule => { 194 | core.summary.addHeading(`Rule: ${rule.description}`, 5) 195 | 196 | if (rule.evaluationResult != "passed") { 197 | if (rule.failureType == "pkgVulnFailure") { 198 | getRulePkgMessage(rule, packages) 199 | } else { 200 | getRuleImageMessage(rule) 201 | } 202 | } 203 | }); 204 | }); 205 | } 206 | }); 207 | 208 | } 209 | 210 | function getRulePkgMessage(rule: Rule, packages: Package[]) { 211 | let table: { data: string, header?: boolean }[][] = [[ 212 | { data: 'Severity', header: true }, 213 | { data: 'Package', header: true }, 214 | { data: 'CVSS Score', header: true }, 215 | { data: 'CVSS Version', header: true }, 216 | { data: 'CVSS Vector', header: true }, 217 | { data: 'Fixed Version', header: true }, 218 | { data: 'Exploitable', header: true }]]; 219 | 220 | rule.failures?.forEach(failure => { 221 | let pkgIndex = failure.pkgIndex ?? 0; 222 | let vulnInPkgIndex = failure.vulnInPkgIndex ?? 0; 223 | 224 | let pkg = packages[pkgIndex]; 225 | let vuln = pkg.vulns?.at(vulnInPkgIndex); 226 | 227 | if (vuln) { 228 | table.push([ 229 | { data: `${vuln.severity.value.toString()}` }, 230 | { data: `${pkg.name}` }, 231 | { data: `${vuln.cvssScore.value.score}` }, 232 | { data: `${vuln.cvssScore.value.version}` }, 233 | { data: `${vuln.cvssScore.value.vector}` }, 234 | { data: `${pkg.suggestedFix || "No fix available"}` }, 235 | { data: `${vuln.exploitable}` }, 236 | ]); 237 | } 238 | }); 239 | 240 | core.summary.addTable(table); 241 | } 242 | 243 | function getRuleImageMessage(rule: Rule) { 244 | let message: string[] = []; 245 | 246 | 247 | rule.failures?.map(failure => failure.remediation); 248 | rule.failures?.forEach(failure => { 249 | message.push(`${failure.remediation}`) 250 | }); 251 | 252 | core.summary.addList(message); 253 | } 254 | -------------------------------------------------------------------------------- /tests/filterPackages.test.ts: -------------------------------------------------------------------------------- 1 | import { filterPackages, Package, FilterOptions, Severity } from '../src/report'; 2 | import { Report } from '../src/report'; 3 | import { Vuln } from '../src/report'; 4 | const fixtureReport : Report = require("../tests/fixtures/report-test.json"); // require is needed here, otherwise the import statement adds a .default attribute to the json 5 | 6 | const basePkg = (vulns: Vuln[] = [], type = "os") => ({ 7 | type, 8 | name: "foo", 9 | version: "1.0", 10 | path: "/foo", 11 | vulns 12 | } as Package); 13 | 14 | const vuln = (severity: string, acceptedRisks: any[] = []): Vuln => ({ 15 | name: "CVE-1234", 16 | severity: { value: severity, sourceName: "sysdig" }, 17 | cvssScore: { value: { version: "3.1", score: 7.5, vector: "AV:N/AC:L/..." }, sourceName: "sysdig" }, 18 | disclosureDate: "2023-01-01", 19 | exploitable: true, 20 | fixedInVersion: "1.2", 21 | publishDateByVendor: { vulndb: "2023-01-01" }, 22 | acceptedRisks, 23 | }); 24 | 25 | describe("filterPackages", () => { 26 | 27 | it("filters by minSeverity", () => { 28 | const pkgs = [ 29 | basePkg([vuln("high")]), 30 | basePkg([vuln("low")]), 31 | ]; 32 | const filters: FilterOptions = { minSeverity: "high" }; 33 | const result = filterPackages(pkgs, filters); 34 | expect(result.length).toBe(1); 35 | expect(result[0].vulns?.[0].severity.value).toBe("high"); 36 | }); 37 | 38 | it("filters by packageTypes", () => { 39 | const pkgs = [ 40 | basePkg([vuln("high")], "os"), 41 | basePkg([vuln("high")], "java"), 42 | ]; 43 | const filters: FilterOptions = { packageTypes: ["java"] }; 44 | const result = filterPackages(pkgs, filters); 45 | expect(result.length).toBe(1); 46 | expect(result[0].type).toBe("java"); 47 | }); 48 | 49 | it("filters by notPackageTypes", () => { 50 | const pkgs = [ 51 | basePkg([vuln("high")], "os"), 52 | basePkg([vuln("high")], "java"), 53 | ]; 54 | const filters: FilterOptions = { notPackageTypes: ["os"] }; 55 | const result = filterPackages(pkgs, filters); 56 | expect(result.length).toBe(1); 57 | expect(result[0].type).toBe("java"); 58 | }); 59 | 60 | it("filters out accepted risks if excludeAccepted is true", () => { 61 | const pkgs = [ 62 | basePkg([vuln("high", [{ index: 1, ref: "ref", id: "id" }])]), 63 | basePkg([vuln("high", [])]), 64 | ]; 65 | const filters: FilterOptions = { excludeAccepted: true }; 66 | const result = filterPackages(pkgs, filters); 67 | expect(result.length).toBe(1); 68 | expect(result[0].vulns?.[0].acceptedRisks).toEqual([]); 69 | }); 70 | 71 | it("removes packages with no vulns after filtering", () => { 72 | const pkgs = [ 73 | basePkg([vuln("low", [{ index: 1, ref: "ref", id: "id" }])]), 74 | ]; 75 | const filters: FilterOptions = { excludeAccepted: true }; 76 | const result = filterPackages(pkgs, filters); 77 | expect(result.length).toBe(0); 78 | }); 79 | }); 80 | 81 | describe("filterPackages with fixture report", () => { 82 | 83 | it("should return only packages with critical vulnerabilities", () => { 84 | const filters: FilterOptions = { minSeverity: "Critical" as Severity }; 85 | const pkgs = fixtureReport.result.packages; 86 | const result = filterPackages(pkgs, filters); 87 | 88 | expect( 89 | result.every(pkg => 90 | pkg.vulns?.some(v => v.severity.value.toLowerCase() === "critical") 91 | ) 92 | ).toBe(true); 93 | 94 | // CVE-2023-38545 is critical 95 | expect(JSON.stringify(result)).toContain("CVE-2023-38545"); 96 | // CVE-2023-38546 is low 97 | expect(JSON.stringify(result)).not.toContain("CVE-2023-38546"); 98 | }); 99 | 100 | it("should exclude packages with only accepted risks when excludeAccepted is true", () => { 101 | const filters: FilterOptions = { excludeAccepted: true, minSeverity: "High" as Severity }; 102 | const pkgs = fixtureReport.result.packages; 103 | const result = filterPackages(pkgs, filters); 104 | 105 | // Vuln with accepted risks should be removed 106 | expect( 107 | result.some(pkg => 108 | pkg.vulns?.some(v => (v.acceptedRisks && v.acceptedRisks.length > 0)) 109 | ) 110 | ).toBe(false); 111 | }); 112 | 113 | it("should filter by packageTypes", () => { 114 | const filters: FilterOptions = { packageTypes: ["os"] }; 115 | const pkgs = fixtureReport.result.packages; 116 | const result = filterPackages(pkgs, filters); 117 | 118 | // All packages must be "os" 119 | expect(result.every(pkg => pkg.type === "os")).toBe(true); 120 | }); 121 | 122 | it("should filter out 'os' packages when notPackageTypes is ['os']", () => { 123 | const filters: FilterOptions = { notPackageTypes: ["os"] }; 124 | const pkgs = fixtureReport.result.packages; 125 | const result = filterPackages(pkgs, filters); 126 | 127 | // No package must be "os" 128 | expect(result.every(pkg => pkg.type !== "os")).toBe(true); 129 | }); 130 | 131 | }); -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import tmp from 'tmp'; 4 | import * as index from ".."; 5 | import * as core from "@actions/core"; 6 | import * as report_test from "./fixtures/report-test.json"; 7 | 8 | import { exec } from "@actions/exec"; 9 | import { ActionInputs } from '../src/action'; 10 | jest.mock("@actions/exec"); 11 | const mockExec = jest.mocked(exec); 12 | 13 | interface TempDir { 14 | tmpDir: string; 15 | cwd: string; 16 | } 17 | 18 | function prepareTemporaryDir(): TempDir { 19 | let tmpDir = tmp.dirSync().name; 20 | let cwd = process.cwd(); 21 | process.chdir(tmpDir); 22 | return { tmpDir: tmpDir, cwd: cwd } 23 | } 24 | 25 | function cleanupTemporaryDir(tmpDir: TempDir) { 26 | fs.rmdirSync(tmpDir.tmpDir, { recursive: true }); 27 | process.chdir(tmpDir.cwd); 28 | } 29 | 30 | const exampleReport = JSON.stringify(report_test); 31 | 32 | describe("input parsing", () => { 33 | let oldEnv: NodeJS.ProcessEnv; 34 | 35 | beforeAll(async () => { 36 | oldEnv = process.env; 37 | await createReportFileIfNotExists(); 38 | }); 39 | 40 | beforeEach(() => { 41 | jest.resetModules(); // most important - it clears the cache 42 | process.env = { ...oldEnv }; // make a copy 43 | }); 44 | 45 | afterEach(() => { 46 | process.env = oldEnv; // restore old env 47 | }); 48 | 49 | it("raises error if no image tag provided", () => { 50 | process.env['INPUT_SYSDIG-SECURE-TOKEN'] = "token"; 51 | expect(() => ActionInputs.parseActionInputs()).toThrow("image-tag is required for VM mode."); 52 | }); 53 | 54 | it("sets default for inputs", () => { 55 | process.env['INPUT_SYSDIG-SECURE-TOKEN'] = "token"; 56 | process.env['INPUT_IMAGE-TAG'] = "image:tag"; 57 | let opts = ActionInputs.parseActionInputs(); 58 | 59 | expect(opts.params).toEqual({ 60 | cliScannerURL: index.cliScannerURL, 61 | cliScannerVersion: "", 62 | registryUser: "", 63 | registryPassword: "", 64 | stopOnFailedPolicyEval: false, 65 | stopOnProcessingError: false, 66 | standalone: false, 67 | dbPath: "", 68 | skipUpload: false, 69 | skipSummary: false, 70 | usePolicies: "", 71 | overridePullString: "", 72 | imageTag: "image:tag", 73 | sysdigSecureToken: "token", 74 | sysdigSecureURL: index.defaultSecureEndpoint, 75 | sysdigSkipTLS: false, 76 | severityAtLeast: undefined, 77 | excludeAccepted: false, 78 | packageTypes: undefined, 79 | notPackageTypes: undefined, 80 | groupByPackage: false, 81 | extraParameters: "", 82 | iacScanPath: "./", 83 | recursive: false, 84 | minimumSeverity: "", 85 | mode: "vm" 86 | }); 87 | }); 88 | 89 | it("parses all inputs", () => { 90 | process.env['INPUT_CLI-SCANNER-URL'] = "https://foo"; 91 | process.env['INPUT_CLI-SCANNER-VERSION'] = "1.0.0"; 92 | process.env['INPUT_REGISTRY-USER'] = "user"; 93 | process.env['INPUT_REGISTRY-PASSWORD'] = "pass"; 94 | process.env['INPUT_STOP-ON-FAILED-POLICY-EVAL'] = "true"; 95 | process.env['INPUT_STOP-ON-PROCESSING-ERROR'] = "true"; 96 | process.env['INPUT_STANDALONE'] = "true"; 97 | process.env['INPUT_DB-PATH'] = "/dbpath"; 98 | process.env['INPUT_SKIP-UPLOAD'] = "true"; 99 | process.env['INPUT_SKIP-SUMMARY'] = "true"; 100 | process.env['INPUT_USE-POLICIES'] = "abcxyz"; 101 | process.env['INPUT_OVERRIDE-PULLSTRING'] = "my-image"; 102 | process.env['INPUT_IMAGE-TAG'] = "image:tag"; 103 | process.env['INPUT_SYSDIG-SECURE-TOKEN'] = "token"; 104 | process.env['INPUT_SYSDIG-SECURE-URL'] = "https://foo"; 105 | process.env['INPUT_SYSDIG-SKIP-TLS'] = "true"; 106 | process.env['INPUT_SEVERITY-AT-LEAST'] = "medium"; 107 | process.env['INPUT_EXCLUDE-ACCEPTED'] = "true"; 108 | process.env['INPUT_PACKAGE-TYPES'] = "os,java"; 109 | process.env['INPUT_NOT-PACKAGE-TYPES'] = "nix"; 110 | process.env['INPUT_GROUP-BY-PACKAGE'] = 'true'; 111 | process.env['INPUT_EXTRA-PARAMETERS'] = "--extra-param"; 112 | process.env['INPUT_IAC-SCAN-PATH'] = "./"; 113 | process.env['INPUT_RECURSIVE'] = "true"; 114 | process.env['INPUT_MINIMUM-SEVERITY'] = "high"; 115 | process.env['INPUT_MODE'] = "vm"; 116 | let opts = ActionInputs.parseActionInputs(); 117 | 118 | expect(opts.params).toEqual({ 119 | "cliScannerURL": "https://foo", 120 | "cliScannerVersion": "1.0.0", 121 | "registryUser": "user", 122 | "registryPassword": "pass", 123 | "stopOnFailedPolicyEval": true, 124 | "stopOnProcessingError": true, 125 | "standalone": true, 126 | "dbPath": "/dbpath", 127 | "skipUpload": true, 128 | "skipSummary": true, 129 | "usePolicies": "abcxyz", 130 | "overridePullString": "my-image", 131 | "imageTag": "image:tag", 132 | "sysdigSecureToken": "token", 133 | "sysdigSecureURL": "https://foo", 134 | "sysdigSkipTLS": true, 135 | "severityAtLeast": "medium", 136 | "excludeAccepted": true, 137 | "packageTypes": "os,java", 138 | "notPackageTypes": "nix", 139 | "groupByPackage": true, 140 | "extraParameters": "--extra-param", 141 | "iacScanPath": "./", 142 | "recursive": true, 143 | "minimumSeverity": "high", 144 | "mode": "vm" 145 | }); 146 | }); 147 | }); 148 | 149 | 150 | describe("execution flags", () => { 151 | 152 | it("uses default flags for VM mode", () => { 153 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", mode: "vm" }).composeFlags(); 154 | expect(flags.envvars.SECURE_API_TOKEN).toMatch("foo-token"); 155 | expect(flags.flags).toMatch(/(^| )image:tag($| )/); 156 | }); 157 | 158 | it("uses default flags for IaC mode", () => { 159 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", mode: "iac", iacScanPath: "/my-special-path" }).composeFlags(); 160 | expect(flags.envvars.SECURE_API_TOKEN).toMatch("foo-token"); 161 | expect(flags.flags).toMatch(/(^| )--iac \/my-special-path($| )/); 162 | }); 163 | 164 | it("adds secure URL flag", () => { 165 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", sysdigSecureURL: "https://foo" }).composeFlags(); 166 | expect(flags.flags).toMatch(/(^| )--apiurl[ =]https:\/\/foo($| )/); 167 | }); 168 | 169 | it("uses standalone mode", () => { 170 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", standalone: true }).composeFlags(); 171 | expect(flags.flags).toMatch(/(^| )--standalone($| )/); 172 | }); 173 | 174 | it("uses registry credentials", () => { 175 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", registryUser: "user", registryPassword: "pass" }).composeFlags(); 176 | expect(flags.envvars.REGISTRY_USER).toMatch('user'); 177 | expect(flags.envvars.REGISTRY_PASSWORD).toMatch('pass'); 178 | }); 179 | 180 | it("uses custom db path", () => { 181 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", dbPath: "/mypath", }).composeFlags(); 182 | expect(flags.flags).toMatch(new RegExp(/(^| )--dbpath[ =]\/mypath($| )/)); 183 | }); 184 | 185 | it("uses skip upload flag", () => { 186 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", skipUpload: true, }).composeFlags(); 187 | expect(flags.flags).toMatch(new RegExp(/(^| )--skipupload($| )/)); 188 | }); 189 | 190 | it("uses custom policies flag", () => { 191 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", usePolicies: "abcxyz", }).composeFlags(); 192 | expect(flags.flags).toMatch(new RegExp(/(^| )--policy[ =]abcxyz($| )/)); 193 | }); 194 | 195 | it("uses --skip-tls flag", () => { 196 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", sysdigSkipTLS: true, }).composeFlags(); 197 | expect(flags.flags).toMatch(new RegExp(/(^| )--skiptlsverify($| )/)); 198 | }); 199 | 200 | it("uses override pullstring flag", () => { 201 | let flags = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", overridePullString: "my-image", }).composeFlags(); 202 | expect(flags.flags).toMatch(new RegExp(/(^| )--override-pullstring[ =]my-image($| )/)); 203 | }); 204 | }); 205 | 206 | describe("scanner pulling", () => { 207 | beforeEach(() => { 208 | jest.resetAllMocks(); 209 | jest.mock("@actions/exec"); 210 | }); 211 | 212 | afterEach(() => { 213 | jest.resetModules(); 214 | }); 215 | 216 | it("pulls the configured scanner", async () => { 217 | mockExec.mockImplementation(jest.fn()); 218 | 219 | await index.pullScanner("https://foo"); 220 | expect(mockExec).toHaveBeenCalledTimes(1); 221 | expect(mockExec.mock.calls[0][0]).toMatch(`wget https://foo -O ./${index.cliScannerName}`); 222 | }); 223 | }); 224 | 225 | describe("scanner execution", () => { 226 | let tmpDir: TempDir; 227 | beforeEach(() => { 228 | jest.resetAllMocks(); 229 | 230 | tmpDir = prepareTemporaryDir(); 231 | 232 | mockExec.mockImplementation(jest.fn()); 233 | 234 | jest.resetModules(); // most important - it clears the cache 235 | // Re-import to ensure fresh module 236 | delete require.cache[require.resolve("..")]; 237 | }); 238 | 239 | afterEach(() => { 240 | cleanupTemporaryDir(tmpDir); 241 | 242 | jest.resetModules(); // most important - it clears the cache 243 | // Re-import to ensure fresh module 244 | delete require.cache[require.resolve("..")]; 245 | }); 246 | 247 | it("invokes the container with the corresponding flags", async () => { 248 | mockExec.mockImplementationOnce((cmdline, args, options) => { 249 | if (options?.listeners) { 250 | options?.listeners?.stdout?.(Buffer.from("foo-id")); 251 | } 252 | return Promise.resolve(0); 253 | }); 254 | 255 | const result = await index.executeScan({ envvars: { SECURE_API_TOKEN: "token" }, flags: "--run1 --run2 image-to-scan" }); 256 | 257 | expect(mockExec).toHaveBeenCalledTimes(2); 258 | expect(mockExec.mock.calls[0][0]).toMatch(`${index.cliScannerName} --run1 --run2 image-to-scan`); 259 | expect(mockExec.mock.calls[1][0]).toMatch(`cat ./${index.cliScannerResult}`); 260 | }); 261 | 262 | it("returns the execution return code", async () => { 263 | mockExec.mockResolvedValueOnce(123); 264 | const result = await index.executeScan({ envvars: { SECURE_API_TOKEN: "token" }, flags: "image-to-scan" }); 265 | expect(result.ReturnCode).toBe(123); 266 | }); 267 | 268 | it("returns the output", async () => { 269 | mockExec.mockImplementation((cmd, args, options) => { 270 | if (options?.listeners) { 271 | options?.listeners?.stdout?.(Buffer.from("foo-output")); 272 | } 273 | return Promise.resolve(0); 274 | }); 275 | 276 | const result = await index.executeScan({ envvars: { SECURE_API_TOKEN: "token" }, flags: "image-to-scan" }); 277 | expect(result.Output).toBe("foo-output"); 278 | }); 279 | }); 280 | 281 | describe("process scan results", () => { 282 | let fs: typeof import("fs"); 283 | let mockCore: jest.Mocked; 284 | 285 | beforeEach(() => { 286 | jest.resetAllMocks(); 287 | jest.mock("@actions/core"); 288 | mockCore = jest.mocked(core); 289 | mockCore.error = jest.fn(); 290 | 291 | fs = require("fs"); 292 | }); 293 | 294 | afterEach(() => { 295 | jest.resetModules(); // most important - it clears the cache 296 | }); 297 | 298 | it("handles error on invalid JSON", async () => { 299 | let scanResult = { 300 | ReturnCode: 0, 301 | Output: 'invalid JSON', 302 | Error: "" 303 | }; 304 | 305 | let opts = ActionInputs.overridingParsedActionInputs({ sysdigSecureToken: "foo-token", imageTag: "image:tag", skipSummary: true, standalone: false, overridePullString: "none" }); 306 | await expect(index.processScanResult(scanResult, opts)).rejects.toThrow(new index.ExecutionError('invalid JSON', '')); 307 | expect(mockCore.error).toHaveBeenCalledTimes(1); 308 | expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Error parsing analysis JSON report")) 309 | }); 310 | 311 | }); 312 | 313 | describe("run the full action", () => { 314 | let tmpDir: TempDir; 315 | let oldEnv: NodeJS.ProcessEnv; 316 | let mockCore: jest.Mocked; 317 | 318 | beforeEach(() => { 319 | oldEnv = process.env; 320 | jest.resetAllMocks(); 321 | 322 | jest.mock("@actions/core"); 323 | mockCore = jest.mocked(core); 324 | mockCore.error = jest.fn(); 325 | mockCore.setFailed = jest.fn(); 326 | 327 | 328 | process.env['INPUT_IMAGE-TAG'] = "image:tag"; 329 | process.env['INPUT_SYSDIG-SECURE-TOKEN'] = "footoken"; 330 | 331 | tmpDir = prepareTemporaryDir(); 332 | 333 | }); 334 | 335 | function setupExecMocks() { 336 | mockExec.mockImplementationOnce((cmdline, args, options) => { 337 | return Promise.resolve(0); 338 | }); 339 | 340 | mockExec.mockImplementationOnce((cmdline, args, options) => { 341 | return Promise.resolve(0); 342 | }); 343 | } 344 | 345 | afterEach(() => { 346 | cleanupTemporaryDir(tmpDir); 347 | 348 | jest.resetModules(); // most important - it clears the cache 349 | 350 | process.env = oldEnv; 351 | }); 352 | 353 | it("ends ok with scan pass", async () => { 354 | setupExecMocks(); 355 | mockExec.mockImplementationOnce((_cmdline, _args, options) => { 356 | return Promise.resolve(0); 357 | }); 358 | 359 | mockExec.mockImplementationOnce((cmdline, args, options) => { 360 | options?.listeners?.stdout?.(Buffer.from(exampleReport)); 361 | return Promise.resolve(0); 362 | }); 363 | 364 | await index.run(); 365 | 366 | expect(core.setFailed).not.toHaveBeenCalled(); 367 | expect(core.error).not.toHaveBeenCalled(); 368 | }); 369 | 370 | it("fails if scan fails", async () => { 371 | process.env['INPUT_STOP-ON-FAILED-POLICY-EVAL'] = "true"; 372 | 373 | setupExecMocks(); 374 | 375 | mockExec.mockImplementationOnce((_cmdline, _args, options) => { 376 | return Promise.resolve(1); 377 | }); 378 | 379 | mockExec.mockImplementationOnce((cmdline, args, options) => { 380 | options?.listeners?.stdout?.(Buffer.from(exampleReport)); 381 | return Promise.resolve(0); 382 | }); 383 | 384 | await index.run(); 385 | 386 | expect(core.setFailed).toHaveBeenCalled(); 387 | }); 388 | 389 | it("ends ok if scan fails but stopOnFailedPolicyEval is false", async () => { 390 | process.env['INPUT_STOP-ON-FAILED-POLICY-EVAL'] = "false"; 391 | 392 | setupExecMocks(); 393 | 394 | mockExec.mockImplementationOnce((_cmdline, _args, options) => { 395 | return Promise.resolve(1); 396 | }); 397 | 398 | mockExec.mockImplementationOnce((cmdline, args, options) => { 399 | options?.listeners?.stdout?.(Buffer.from(exampleReport)); 400 | return Promise.resolve(0); 401 | }); 402 | 403 | await index.run(); 404 | 405 | expect(core.setFailed).not.toHaveBeenCalled(); 406 | }); 407 | 408 | it("fails if scanner has wrong parameters and stopOnProcessingError is true", async () => { 409 | process.env['INPUT_STOP-ON-PROCESSING-ERROR'] = "true"; 410 | 411 | mockExec.mockImplementationOnce((cmdline, args, options) => { 412 | return Promise.resolve(2); 413 | }); 414 | 415 | await index.run(); 416 | 417 | expect(core.setFailed).toHaveBeenCalled(); 418 | }); 419 | 420 | it("fails on unexpected error and stopOnProcessingError is true", async () => { 421 | process.env['INPUT_STOP-ON-PROCESSING-ERROR'] = "true"; 422 | 423 | setupExecMocks(); 424 | 425 | mockExec.mockImplementationOnce((cmdline, args, options) => { 426 | options?.listeners?.stdout?.(Buffer.from(exampleReport)); 427 | return Promise.resolve(123); 428 | }); 429 | 430 | await index.run(); 431 | 432 | expect(core.setFailed).toHaveBeenCalled(); 433 | }); 434 | 435 | it("ends ok if scan fails but stopOnProcessingError is false", async () => { 436 | process.env['INPUT_STOP-ON-PROCESSING-ERROR'] = "false"; 437 | 438 | setupExecMocks(); 439 | 440 | mockExec.mockImplementationOnce((_cmdline, _args, options) => { 441 | return Promise.resolve(123); 442 | }); 443 | 444 | mockExec.mockImplementationOnce((cmdline, args, options) => { 445 | options?.listeners?.stdout?.(Buffer.from(exampleReport)); 446 | return Promise.resolve(0); 447 | }); 448 | 449 | await index.run(); 450 | 451 | expect(core.setFailed).not.toHaveBeenCalled(); 452 | }); 453 | 454 | it("allows override of inline-scan image", async () => { 455 | process.env['INPUT_OVERRIDE-PULLSTRING'] = "my-custom-image:latest"; 456 | 457 | mockExec.mockImplementation(jest.fn(() => { 458 | return Promise.resolve(0); 459 | })); 460 | 461 | await index.run(); 462 | expect(mockExec).toHaveBeenCalledTimes(4); 463 | expect(mockExec.mock.calls[2][0]).toMatch(`${index.cliScannerName} --apiurl https://secure.sysdig.com/ --override-pullstring=my-custom-image:latest --json-scan-result=scan-result.json image:tag`); 464 | }); 465 | }); 466 | 467 | 468 | async function createReportFileIfNotExists() { 469 | const summary_file = process.env.GITHUB_STEP_SUMMARY || "/tmp/github_summary.html"; 470 | const promise = new Promise((resolve, reject) => { 471 | if (fs.existsSync(summary_file)) { 472 | return resolve(undefined); 473 | } 474 | fs.writeFile(summary_file, "", (err) => { 475 | if (err == null) { 476 | return resolve(undefined); 477 | } 478 | return reject(err); 479 | }); 480 | }); 481 | 482 | return promise; 483 | } 484 | 485 | describe("ActionInputs validation", () => { 486 | it("accepts valid severities", () => { 487 | const inputs = { sysdigSecureToken: "t", imageTag: "foo", severityAtLeast: "critical", mode: "vm" }; 488 | expect(() => ActionInputs.overridingParsedActionInputs(inputs)).not.toThrow(); 489 | }); 490 | it("rejects invalid severityAtLeast", () => { 491 | const inputs = { sysdigSecureToken: "t", imageTag: "foo", severityAtLeast: "severe", mode: "vm" }; 492 | expect(() => ActionInputs.overridingParsedActionInputs(inputs)).toThrow(/Invalid severity-at-least/); 493 | }); 494 | }); -------------------------------------------------------------------------------- /tests/sarif.test.ts: -------------------------------------------------------------------------------- 1 | import { vulnerabilities2SARIF } from "../src/sarif" 2 | import { Report } from "../src/report"; 3 | const fixtureReport: Report = require("../tests/fixtures/report-test.json"); // require is needed here, otherwise the import statement adds a .default attribute to the json 4 | const fixtureSarif = require("../tests/fixtures/sarif-test.json"); // require is needed here, otherwise the import statement adds a .default attribute to the json 5 | 6 | describe("input parsing", () => { 7 | describe("when the result contains vulnerabilities", () => { 8 | it("returns the sarif format", () => { 9 | const someReport: Report = fixtureReport; 10 | const groupByPackage = false; 11 | const sarifGenerated = vulnerabilities2SARIF(someReport, groupByPackage) 12 | 13 | expect(sarifGenerated).toEqual(fixtureSarif); 14 | }) 15 | }) 16 | 17 | describe("when the result does not contain vulnerabilities", () => { 18 | it("returns the sarif format with the minimal response", () => { 19 | let someReportWithoutVulns: Report = removeVulnsFromReport(fixtureReport); 20 | 21 | const groupByPackage = false; 22 | const sarifGenerated = vulnerabilities2SARIF(someReportWithoutVulns, groupByPackage) 23 | 24 | expect(sarifGenerated).toEqual({ 25 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 26 | version: "2.1.0", 27 | runs: [{ 28 | tool: { 29 | driver: { 30 | name: "sysdig-cli-scanner", 31 | fullName: "Sysdig Vulnerability CLI Scanner", 32 | informationUri: "https://docs.sysdig.com/en/docs/installation/sysdig-secure/install-vulnerability-cli-scanner", 33 | version: "5.2.0", 34 | semanticVersion: "5.2.0", 35 | dottedQuadFileVersion: "5.2.0.0", 36 | rules: [] 37 | } 38 | }, 39 | logicalLocations: [ 40 | { 41 | name: "container-image", 42 | fullyQualifiedName: "container-image", 43 | kind: "namespace" 44 | } 45 | ], 46 | results: [], 47 | columnKind: "utf16CodeUnits", 48 | properties: { 49 | architecture: "amd64", 50 | baseOs: "alpine 3.18.0", 51 | digest: "sha256:345ba1354949b1c66802fef1d048e89399d6f0116d4eea31d81c789b69b30b29", 52 | imageId: "sha256:2208f3cc77d0c6bc66fd8ff18e25628df8e9d759e7aa82fe9c7c84f254ff0237", 53 | layersCount: 12, 54 | os: "linux", 55 | pullString: "jenkins/jenkins:2.401.1-alpine", 56 | resultId: "17e69f1c42f0d322164d17ab30e34730", 57 | resultUrl: "https://secure.sysdig.com/#/vulnerabilities/results/17e69f1c42f0d322164d17ab30e34730/overview", 58 | size: 259845632, 59 | } 60 | }] 61 | }); 62 | }) 63 | }) 64 | }) 65 | 66 | const removeVulnsFromReport = (report: Report): Report => { 67 | return { 68 | ...report, 69 | result: { 70 | ...report.result, 71 | packages: report.result.packages.map(pkg => ({ 72 | ...pkg, 73 | vulns: [], 74 | })) 75 | } 76 | }; 77 | }; 78 | 79 | 80 | describe("SARIF filtering", () => { 81 | it("respects minSeverity", () => { 82 | const sarif = vulnerabilities2SARIF(fixtureReport, false, { minSeverity: "critical" }); 83 | 84 | expect(JSON.stringify(sarif)).toContain("Critical"); 85 | expect(JSON.stringify(sarif)).toContain("CVE-2023-38545"); 86 | expect(JSON.stringify(sarif)).not.toContain("High"); 87 | expect(JSON.stringify(sarif)).not.toContain("CVE-2023-38039"); 88 | expect(JSON.stringify(sarif)).not.toContain("Medium"); 89 | expect(JSON.stringify(sarif)).not.toContain("CVE-2023-42364"); 90 | }); 91 | 92 | it("respects packageTypes", () => { 93 | const sarif = vulnerabilities2SARIF(fixtureReport, false, { packageTypes: ["os"] }); 94 | expect(JSON.stringify(sarif)).toContain("CVE-2023-42365"); 95 | expect(JSON.stringify(sarif)).not.toContain("CVE-2023-42503"); 96 | }); 97 | 98 | it("respects excludeAccepted", () => { 99 | const sarif = vulnerabilities2SARIF(fixtureReport, false, { excludeAccepted: true }); 100 | expect(JSON.stringify(sarif)).not.toContain("CVE-2016-1000027"); 101 | }); 102 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./build", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 63 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 77 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 79 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 81 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 82 | 83 | /* Type Checking */ 84 | "strict": true, /* Enable all strict type-checking options. */ 85 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 86 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 87 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 90 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 91 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 92 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 93 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 94 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 95 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 96 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 97 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 98 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 99 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 100 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 101 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 102 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 103 | 104 | /* Completeness */ 105 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 106 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 107 | }, 108 | // "include": ["./**/*.ts", "./**/*.json"] 109 | } 110 | --------------------------------------------------------------------------------