├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ └── pr-housekeeping.yml ├── .gitignore ├── .gitleaksignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.json ├── .releaserc ├── .snyk ├── CONTRIBUTING.md ├── Contributor-Agreement.md ├── LICENSE ├── NOTICE ├── README.md ├── catalog-info.yaml ├── images ├── extension-icon.png ├── report-failed.png └── report-passed.png ├── jest.config.js ├── marketplace.md ├── ops └── deploy │ ├── __tests__ │ ├── test-cli-args.ts │ ├── test-deploy.ts │ └── test-json-file-updater.ts │ ├── cli-args.ts │ ├── deploy.ts │ ├── get-current-dev-ext-version.ts │ ├── get-next-dev-ext-version.ts │ ├── install-extension-to-dev-org.ts │ ├── jest.config.js │ ├── lib │ ├── azure-devops │ │ ├── builds.ts │ │ ├── extensions.ts │ │ └── index.ts │ └── sleep.ts │ ├── package-lock.json │ ├── package.json │ ├── run-test-pipelines.ts │ ├── test-builds.json │ └── tsconfig.json ├── package-lock.json ├── package.json ├── scripts ├── ci-build.sh ├── ci-deploy-dev.sh ├── ci-deploy-preview.sh ├── ci-deploy-prod.sh ├── ci-deploy.sh ├── ci-update-task-json-preview.js ├── ci-update-task-json-prod.js ├── recovery-task-json-dev.js └── update-task-json-dev.js ├── snykTask ├── .snyk ├── icon.png ├── package-lock.json ├── package.json ├── src │ ├── __tests__ │ │ ├── install │ │ │ └── index.test.ts │ │ ├── task-lib.test.ts │ │ ├── task-version.test.ts │ │ ├── test-auth-token-retrieved.ts │ │ └── test-task-args.ts │ ├── index.ts │ ├── install │ │ └── index.ts │ ├── lib │ │ └── sanitize-version-input.ts │ ├── task-args.ts │ ├── task-lib.ts │ └── task-version.ts ├── task.json ├── test │ └── fixtures │ │ ├── code-test-error-issues.json │ │ ├── code-test-no-issues.json │ │ ├── code-test-none-issues.json │ │ ├── code-test-note-issues.json │ │ ├── code-test-warning-issues.json │ │ ├── container-app-vulnerabilities-critical.json │ │ ├── container-app-vulnerabilities-medium.json │ │ ├── golang-no-code-issues │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ │ ├── high-vulnerabilities.json │ │ ├── low-vulnerabilities.json │ │ ├── single-project-high-vulnerabilities.json │ │ ├── somehtml.html │ │ ├── somehtmlAfterGlobal.html │ │ ├── somejson.json │ │ └── somejsonAfterNonglobal.json └── tsconfig.json ├── ui ├── enhancer │ ├── __tests__ │ │ └── snyk-report.test.ts │ ├── detect-vulns.ts │ ├── generate-report-title.ts │ ├── snyk-report.ts │ └── tsconfig.json └── snyk-report-tab.html ├── vss-extension-dev.json ├── vss-extension-preview.json └── vss-extension.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | prodsec: snyk/prodsec-orb@1 5 | 6 | jobs: 7 | security-scans: 8 | resource_class: small 9 | docker: 10 | - image: cimg/base:stable 11 | steps: 12 | - checkout 13 | - prodsec/security_scans: 14 | mode: auto 15 | 16 | test: 17 | docker: 18 | - image: circleci/node:lts 19 | working_directory: ~/repo 20 | steps: 21 | - checkout 22 | - run: 23 | name: Build 24 | command: | 25 | npm run build:clean 26 | - run: 27 | name: Checks 28 | command: | 29 | npm run test:checks 30 | - run: 31 | name: Snyk Test 32 | command: | 33 | npm run test:snyk 34 | - run: 35 | name: Unit Tests 36 | command: | 37 | npm run test:unit 38 | 39 | deploy_dev: 40 | docker: 41 | - image: cimg/node:lts 42 | environment: 43 | DEV_AZ_EXTENSION_ID: 'dev-security-scan-test' 44 | DEV_AZ_EXTENSION_NAME: 'Dev - Snyk Security Scan' 45 | DEV_AZ_TASK_FRIENDLY_NAME: 'Dev - Snyk Security Scan' 46 | DEV_AZ_TASK_NAME: 'DevSnykSecurityScan' 47 | 48 | working_directory: ~/repo 49 | steps: 50 | - checkout 51 | 52 | - run: 53 | name: Show Node Environment 54 | command: | 55 | node --version 56 | npm --version 57 | 58 | - run: 59 | name: Run Build 60 | command: | 61 | npm run build:clean 62 | 63 | - run: 64 | name: Build and Deploy to Test Environment 65 | command: | 66 | echo DEV_AZ_ORG: $DEV_AZ_ORG # Set in CCI Project Settings 67 | echo DEV_AZ_PUBLISHER: $DEV_AZ_PUBLISHER # Set in CCI Project Settings 68 | 69 | echo DEV_AZ_EXTENSION_ID: $DEV_AZ_EXTENSION_ID 70 | echo DEV_AZ_EXTENSION_NAME: $DEV_AZ_EXTENSION_NAME 71 | echo DEV_AZ_TASK_FRIENDLY_NAME: $DEV_AZ_TASK_FRIENDLY_NAME 72 | echo DEV_AZ_TASK_NAME: $DEV_AZ_TASK_NAME 73 | 74 | npm run deploy:compile 75 | NEXT_DEV_VERSION=$(node ./ops/deploy/dist/get-next-dev-ext-version.js) 76 | if [[ $? -eq 0 ]]; then 77 | echo NEXT_DEV_VERSION: $NEXT_DEV_VERSION 78 | else 79 | echo "no current version. Setting NEXT_DEV_VERSION to 0.0.1" 80 | NEXT_DEV_VERSION=0.0.1 81 | fi 82 | 83 | echo "Deploying to dev with ${NEXT_DEV_VERSION} ${AZ_ORG}" 84 | scripts/ci-deploy.sh $NEXT_DEV_VERSION $DEV_AZ_ORG 85 | 86 | - run: 87 | name: Create renamed copy of the vsix bundle 88 | command: | 89 | cp *.vsix dev-extension-artifact.vsix 90 | ls -la dev-extension-artifact.vsix 91 | 92 | - store_artifacts: 93 | path: ./dev-extension-artifact.vsix 94 | 95 | - run: 96 | name: Launch Test Pipelines 97 | command: | 98 | node ./ops/deploy/dist/run-test-pipelines.js 99 | 100 | deploy_preview: 101 | docker: 102 | - image: cimg/node:lts 103 | environment: 104 | AZ_EXTENSION_ID: 'preview-snyk-security-scan' 105 | AZ_EXTENSION_NAME: '(Preview) Snyk Security Scan' 106 | AZ_TASK_NAME: 'PreviewSnykSecurityScan' 107 | AZ_TASK_FRIENDLY_NAME: '(Preview) Snyk Security Scan' 108 | AZ_PUBLISHER: 'Snyk' 109 | working_directory: ~/repo 110 | steps: 111 | - checkout 112 | - run: 113 | name: Show Node Environment 114 | command: | 115 | node --version 116 | npm --version 117 | - run: 118 | name: Run Build 119 | command: | 120 | npm run build:clean 121 | - run: 122 | name: Build and Deploy to Preview Environment 123 | command: | 124 | export AZURE_DEVOPS_EXT_PAT=$PROD_AZURE_DEVOPS_EXT_PAT 125 | 126 | echo PREVIEW_AZ_EXTENSION_ID: $AZ_EXTENSION_ID 127 | echo PREVIEW_AZ_EXTENSION_NAME: $AZ_EXTENSION_NAME 128 | echo PREVIEW_AZ_TASK_NAME: $AZ_TASK_NAME 129 | echo PREVIEW_AZ_PUBLISHER: $AZ_PUBLISHER 130 | 131 | npm run deploy:compile 132 | 133 | VERSION=$(date +"%Y.%-m.%-d%H%M") 134 | echo "Deploying to Preview: ${VERSION}" 135 | 136 | chmod +x scripts/ci-deploy-preview.sh 137 | scripts/ci-deploy-preview.sh $VERSION 138 | deploy_prod: 139 | docker: 140 | - image: circleci/node:lts 141 | environment: 142 | AZ_EXTENSION_ID: 'snyk-security-scan' 143 | AZ_EXTENSION_NAME: 'Snyk Security Scan' 144 | AZ_PUBLISHER: 'Snyk' 145 | 146 | working_directory: ~/repo 147 | steps: 148 | - checkout 149 | - run: 150 | name: Setup Env Vars 151 | command: | 152 | export AZURE_DEVOPS_EXT_PAT=$PROD_AZURE_DEVOPS_EXT_PAT 153 | echo AZ_EXTENSION_ID: $AZ_EXTENSION_ID 154 | echo AZ_EXTENSION_NAME: $AZ_EXTENSION_NAME 155 | echo AZ_PUBLISHER: $AZ_PUBLISHER 156 | 157 | - run: 158 | name: Build 159 | command: | 160 | npm run build:clean 161 | 162 | - run: 163 | name: Create Extension 164 | command: | 165 | export AZURE_DEVOPS_EXT_PAT=$PROD_AZURE_DEVOPS_EXT_PAT 166 | npx semantic-release 167 | 168 | - run: 169 | name: Create renamed copy of the vsix bundle 170 | command: | 171 | if ls *.vsix 1>/dev/null 2>&1; then 172 | cp *.vsix prod-extension-artifact.vsix 173 | ls -la prod-extension-artifact.vsix 174 | else 175 | echo "No new version detected; skipping .vsix file copy step." 176 | fi 177 | 178 | - store_artifacts: 179 | path: ./prod-extension-artifact.vsix 180 | 181 | workflows: 182 | build_and_test: 183 | jobs: 184 | - prodsec/secrets-scan: 185 | name: Scan repository for secrets 186 | context: 187 | - snyk-bot-slack 188 | channel: cli-alerts 189 | 190 | - security-scans: 191 | context: devex_cli 192 | 193 | - test 194 | - deploy_dev: 195 | requires: 196 | - test 197 | filters: 198 | branches: 199 | ignore: main 200 | 201 | - deploy_prod: 202 | requires: 203 | - test 204 | filters: 205 | branches: 206 | only: main 207 | manual_approval: 208 | jobs: 209 | - approve-preview-deployment: 210 | name: 'Deploy the branch as Preview?' 211 | type: approval 212 | filters: 213 | branches: 214 | ignore: main 215 | - deploy_preview: 216 | requires: 217 | - 'Deploy the branch as Preview?' 218 | filters: 219 | branches: 220 | ignore: main 221 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | settings: { cache: true }, 19 | parser: '@typescript-eslint/parser', 20 | // Pending https://github.com/typescript-eslint/typescript-eslint/issues/389 21 | // parserOptions: { 22 | // project: './tsconfig.json', 23 | // }, 24 | env: { 25 | node: true, 26 | es2020: true, 27 | 'jest/globals': true, 28 | }, 29 | plugins: ['jest', '@typescript-eslint'], 30 | extends: [ 31 | 'eslint:recommended', 32 | 'plugin:@typescript-eslint/eslint-recommended', 33 | 'plugin:@typescript-eslint/recommended', 34 | 'prettier', 35 | 'prettier/@typescript-eslint', 36 | ], 37 | rules: { 38 | '@typescript-eslint/explicit-function-return-type': 0, 39 | '@typescript-eslint/no-explicit-any': 0, 40 | 41 | // non-null assertions compromise the type safety somewhat, but many 42 | // our types are still imprecisely defined and we don't use noImplicitAny 43 | // anyway, so for the time being assertions are allowed 44 | '@typescript-eslint/no-non-null-assertion': 1, 45 | '@typescript-eslint/ban-ts-ignore': 'off', 46 | 47 | '@typescript-eslint/no-var-requires': 0, 48 | '@typescript-eslint/no-use-before-define': 0, 49 | '@typescript-eslint/no-inferrable-types': 'off', 50 | 'no-prototype-builtins': 0, 51 | 'require-atomic-updates': 0, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @snyk/cli 2 | -------------------------------------------------------------------------------- /.github/workflows/pr-housekeeping.yml: -------------------------------------------------------------------------------- 1 | name: Check for stale PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Every day at midnight 6 | workflow_dispatch: 7 | 8 | jobs: 9 | close-stale: 10 | runs-on: ubuntu-latest 11 | name: 'Label and close stale PRs after no activity for a long time' 12 | steps: 13 | - name: 'Close stale PRs' 14 | uses: actions/stale@v9.1.0 15 | with: 16 | stale-pr-label: Stale 17 | days-before-stale: 30 18 | days-before-close: 2 19 | stale-pr-message: "Your PR has not had any activity for 30 days. In 2 days I'll close it. Make some activity to remove this." 20 | close-pr-message: "Your PR has now been stale for 2 days. I'm closing it." 21 | delete-branch: true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | snykTask/dist 2 | scripts/dist 3 | node_modules 4 | .azure-pipeline-jobs 5 | cycle.sh 6 | .ops.md 7 | .DS_Store 8 | .idea 9 | .taskkey 10 | /.mocha_tests/ 11 | snykTask/coverage 12 | *.bak 13 | *.vsix 14 | .eslintcache 15 | .ops 16 | ops/deploy/dist 17 | -------------------------------------------------------------------------------- /.gitleaksignore: -------------------------------------------------------------------------------- 1 | # add false positives here 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/gitleaks/gitleaks 5 | rev: v8.17.0 6 | hooks: 7 | - id: gitleaks 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | fixtures 4 | test-output 5 | Contributor-Agreement.md 6 | CONTRIBUTING.md 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "htmlWhitespaceSensitivity": "ignore" 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "repositoryUrl": "https://github.com/snyk/snyk-azure-pipelines-task", 6 | "plugins": [ 7 | [ 8 | "@semantic-release/exec", 9 | { 10 | "publishCmd": "scripts/ci-deploy.sh ${nextRelease.version}" 11 | } 12 | ], 13 | [ 14 | "@semantic-release/git", 15 | { 16 | "assets": [], 17 | "message": "chore(release): ${nextRelease.version}" 18 | } 19 | ], 20 | "@semantic-release/commit-analyzer", 21 | "@semantic-release/release-notes-generator", 22 | "@semantic-release/changelog", 23 | "@semantic-release/github" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.25.0 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-MOCKERY-3043117: 6 | - '*': 7 | reason: No upgrade available 8 | expires: 2024-07-18T00:00:00.000Z 9 | created: 2023-07-18T12:52:43.840Z 10 | SNYK-JS-INFLIGHT-6095116: 11 | - '*': 12 | reason: No upgrade available 13 | expires: 2024-06-01T00:00:00.000Z 14 | created: 2023-12-04T08:42:17.557Z 15 | patch: {} 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development 4 | ### Setup 5 | Run `npm run build` in the root folder. All tooling prerequisites (Node.js, TypeScript etc.) can be seen [here](https://docs.microsoft.com/en-us/azure/devops/extend/develop/add-build-task?view=azure-devops#prerequisites) and should be installed. 6 | 7 | ### Test and Run 8 | Unit tests can be run via `npm run test:unit` command. 9 | 10 | To run the code, a GitHub PR against `main` should be raised with the committed code to the branch PR. The PR runs deployment script with deploy to development environment. The script builds the code that's added as part of your change and installs it in Azure DevOps organization as an extension that can be added to run a pipeline. 11 | 12 | ### Local debugging 13 | 14 | A number of environment variable are required for debugging, here's an example launch config for `Visual Studio Code` that sets mandatory parameters such as `AGENT_TEMPDIRECTORY`, `INPUT_failOnIssues` and `INPUT_authToken` 15 | 16 | ```json 17 | { 18 | "version": "0.2.0", 19 | "configurations": [ 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Launch Program", 24 | "program": "${workspaceFolder}/snykTask/src/index.ts", 25 | "env": { 26 | "AGENT_TEMPDIRECTORY": "some/temp/path", 27 | "INPUT_failOnIssues": "true", 28 | "INPUT_authToken": "your-auth-token-guid-from-portal", 29 | "INPUT_targetFile": "path-to-visual-studio-solution.sln", 30 | "INPUT_organization": "your-org-guid-from-portal", 31 | "INPUT_monitorWhen": "never", 32 | "INPUT_severityThreshold": "low", 33 | "INPUT_failOnThreshold": "critical", 34 | "NODE_OPTIONS": null 35 | }, 36 | "outFiles": [ 37 | "${workspaceFolder}/**/*.js" 38 | ] 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | ## Release 45 | The release process is fully-automated: all you need to do is create a PR to merge into `main`. 46 | 47 | ## Contributor Agreement 48 | A pull-request will only be considered for merging into the upstream codebase after you have signed our [contributor agreement](https://github.com/snyk/snyk-azure-pipelines-task/blob/main/Contributor-Agreement.md), assigning us the rights to the contributed code and granting you a license to use it in return. If you submit a pull request, you will be prompted to review and sign the agreement with one click (we use [CLA assistant](https://cla-assistant.io/)). 49 | 50 | ## Commit messages 51 | 52 | Commit messages must follow the [Angular-style](https://github.com/angular/angular.js/blob/main/CONTRIBUTING.md#commit-message-format) commit format (but excluding the scope). 53 | 54 | i.e: 55 | 56 | ```text 57 | fix: minified scripts being removed 58 | 59 | Also includes tests 60 | ``` 61 | 62 | This will allow for the automatic changelog to generate correctly. 63 | 64 | ### Commit types 65 | 66 | Must be one of the following: 67 | 68 | * **feat**: A new feature 69 | * **fix**: A bug fix 70 | * **docs**: Documentation only changes 71 | * **test**: Adding missing tests 72 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 73 | * **refactor**: A code change that neither fixes a bug nor adds a feature 74 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 75 | * **perf**: A code change that improves performance 76 | 77 | To release a major you need to add `BREAKING CHANGE: ` to the start of the body and the detail of the breaking change. 78 | 79 | ## Code standards 80 | 81 | Ensure that your code adheres to the included `.eslintrc` config by running `npm run test:checks`. 82 | 83 | Fix any `prettier` violations reported before pushing by running `npm run format` 84 | 85 | ## Sending pull requests 86 | 87 | - add tests for newly added code (and try to mirror directory and file structure if possible) or fixes 88 | - spell check 89 | - PRs will not be code reviewed unless all tests are passing 90 | 91 | *Important:* when fixing a bug, please commit a **failing test** first so that CI (or I can) can show the code failing. Once that commit is in place, then commit the bug fix, so that we can test *before* and *after*. 92 | -------------------------------------------------------------------------------- /Contributor-Agreement.md: -------------------------------------------------------------------------------- 1 | This Contributor Licence Agreement (“Agreement”) sets out the terms under which contributions are made to open source projects of Snyk Ltd (“Snyk”) by or on behalf of the Contributor. This Agreement is legally binding on the Contributor. 2 | 3 | 4 | Who the “Contributor” is depends on whether the person submitting the contribution is a private individual acting on their own behalf, or is acting on behalf of someone else (for example, their employer). The “Contributor” in this Agreement is therefore either: (i) if the individual who Submits a Contribution does so on behalf of their employer or another Legal Entity, any Legal Entity on behalf of whom a Contribution has been received by Snyk; or in all other cases (ii) the individual who Submits a Contribution to Snyk. "Legal Entity" means an entity which is not a natural person (for example, a limited company or corporation). 5 | 6 | 7 | ** 1. Interpretation** 8 | 9 | 10 | The following definitions and rules of interpretation apply in this Agreement. 11 | 12 | 13 | 1.1 Definitions: 14 | 15 | **Affiliates**: means, in respect of a Legal Entity, any other Legal Entities that control, are controlled by, or under common control with that Legal Entity. 16 | 17 | 18 | **Contribution**: means any software or code that is Submitted by the Contributor to Snyk for inclusion in a Project. 19 | 20 | 21 | **Copyright**: all copyright and rights in the nature of copyright subsisting in the Contribution in any part of the world, to which the Contributor is, or may become, entitled. 22 | 23 | 24 | **Effective Date**: the earlier of the date on which the Contributor Submits the Contribution, or the date of the Contributor’s acceptance of this Agreement. 25 | 26 | 27 | **Patent Rights**: any patent claims which the Contributor or its Affiliates owns, controls or has the right to grant, now or in the future, to the extent infringed by the exercise of the rights licensed under this Agreement. 28 | 29 | 30 | **Project**: a software project to which the Contribution is Submitted. 31 | 32 | 33 | **Submit**: means to submit or send to Snyk or its representatives by any form of electronic, verbal, or written communication, for example, by means of code repositories or control systems, and issue tracking systems, that are managed by or on behalf of Snyk. 34 | 35 | 36 | **2. Licence Grant** 37 | 38 | 39 | 2.1 Copyright: The Contributor grants to Snyk a perpetual, irrevocable, worldwide, transferable, fully sublicenseable through multiple tiers, fee-free, non-exclusive licence under the Copyright to do the following acts, subject to, and in accordance with, the terms of this Agreement: to reproduce, prepare derivative works of, publicly display, publicly perform, communicate to the public, and distribute by any means Contributions and such derivative works. 40 | 41 | 42 | 2.2. Patent Rights: The Contributor grants to Snyk a perpetual, irrevocable, worldwide, transferable, fully sublicenseable through multiple tiers, fee-free, non-exclusive licence under the Patent Rights to do the following acts, subject to, and in accordance with, the terms of this Agreement: to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with a Project (and portions of such combination). 43 | 44 | 45 | 2.3 The Contributor acknowledges that Snyk is not obliged to include the Contribution in any Project. 46 | 47 | 48 | 2.4 If Snyk includes the Contribution in a Project, Snyk may license the Contribution under any licence terms, including copyleft, permissive, commercial, or proprietary licenses, provided that it shall also license the Contribution under the terms of any licenses which are approved by the Open Source Initiative on or after the Effective Date, including both permissive and copyleft licenses, whether or not such licenses are subsequently disapproved (including any right to adopt any future version of a license if permitted). 49 | 50 | 51 | 2.5 In the event that any moral rights apply in respect of the Contribution, the Contributor, being the sole author of the Contribution, waives all moral rights in respect of the use to be made of the Contribution under this Agreement to which the Contributor may now or at any future time be entitled. 52 | 53 | 54 | **3. Warranties and Disclaimers** 55 | 56 | 57 | 3.1 The Contributor warrants and represents that: 58 | 59 | 60 | (a) it is the sole owner of the Copyright and any Patent Rights and legally entitled to grant the licence in section 2; 61 | 62 | (b) the Contribution is its own original creation; 63 | 64 | 65 | (c) the licence in section 2 does not conflict with or infringe any rights granted by the Contributor or (if applicable) its Affiliates; and 66 | 67 | 68 | (d) it is not aware of any claims, suits, or actions in respect of the Contribution. 69 | 70 | 71 | 3.2 All other conditions, warranties or other terms which might have effect between the parties in respect of the Contribution or be implied or incorporated into this Agreement are excluded. 72 | 73 | 3.3 The Contributor is not required to provide any support for the Contribution. 74 | 75 | 76 | **4. Other important terms** 77 | 78 | 79 | 4.1 Assignment/Transfer: Snyk may assign and transfer all of its rights and obligations under this Agreement to any person. 80 | 81 | 82 | 4.2 Further Assurance: The Contributor shall at Snyk’s expense execute and deliver such documents and perform such acts as may reasonably be required by Snyk for the purpose of giving full effect to this Agreement. 83 | 84 | 85 | 4.3 Agreement: This Agreement constitutes the entire Agreement between the parties and supersedes and extinguishes all previous Agreements, promises, assurances, warranties, representations and understandings between them, whether written or oral, relating to its subject matter. 86 | 87 | 88 | 4.4 Governing law: This Agreement and any dispute or claim (including non-contractual disputes or claims) arising out of or in connection with it or its subject matter or formation shall be governed by and construed in accordance with the law of England and Wales. 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Snyk Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: snyk-azure-pipelines-task 5 | annotations: 6 | github.com/project-slug: snyk/snyk-azure-pipelines-task 7 | github.com/team-slug: snyk/cli 8 | spec: 9 | type: supply-chain-tooling 10 | lifecycle: '-' 11 | owner: cli 12 | -------------------------------------------------------------------------------- /images/extension-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/snyk-azure-pipelines-task/f5212028be27d4ff47381cc299489fbe5d165e41/images/extension-icon.png -------------------------------------------------------------------------------- /images/report-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/snyk-azure-pipelines-task/f5212028be27d4ff47381cc299489fbe5d165e41/images/report-failed.png -------------------------------------------------------------------------------- /images/report-passed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/snyk-azure-pipelines-task/f5212028be27d4ff47381cc299489fbe5d165e41/images/report-passed.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | roots: ['/snykTask/src/', '/ui/'], 19 | transform: { 20 | '^.+\\.tsx?$': 'ts-jest', 21 | }, 22 | testPathIgnorePatterns: [ 23 | '/node_modules/', 24 | 'snykTask/src/__tests__/_test-mock-config-*', 25 | 'snykTask/src/__tests__/test-task.ts', 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /marketplace.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This task allows you to easily run Snyk scans within your Azure Pipeline jobs. You will need to first [create a Snyk account](https://snyk.io/login). 4 | There are two major options: 5 | 6 | - Snyk scan for application dependencies. This will look at manifest files. 7 | - Snyk scan for container images. This will look at Docker images. 8 | 9 | In addition to running a Snyk security scan, you also have the option to monitor your application / container, in which case the dependency tree or container image metadata will be posted to your Snyk account for ongoing monitoring. 10 | 11 | ## Documentation 12 | 13 | Please refer to [https://snyk.io/docs/](https://snyk.io/docs/) for documentation on using Snyk. 14 | 15 | ## Support 16 | 17 | For support issues, please visit our [support portal](https://support.snyk.io/) or contact `support@snyk.io`. 18 | -------------------------------------------------------------------------------- /ops/deploy/__tests__/test-cli-args.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { parseInputParameters, Command, DeployTarget } from '../cli-args'; 18 | 19 | class FakeExitError extends Error { 20 | constructor(message?: string) { 21 | super(message); 22 | } 23 | } 24 | 25 | let mockProcessExit; 26 | 27 | beforeEach(() => { 28 | //@ts-ignore 29 | mockProcessExit = jest 30 | .spyOn(process, 'exit') 31 | .mockImplementation((exitCode) => { 32 | throw new FakeExitError('fake exit'); 33 | }); 34 | }); 35 | 36 | afterEach(() => { 37 | mockProcessExit.mockRestore(); 38 | }); 39 | 40 | test('fail if no command (version-check/command) is set', () => { 41 | expect(() => { 42 | const inputArgs = []; 43 | parseInputParameters(inputArgs); 44 | }).toThrow(FakeExitError); 45 | }); 46 | 47 | test('version-check arg parsing works', () => { 48 | const inputArgs = ['version-check']; 49 | const parsedArgs = parseInputParameters(inputArgs); 50 | expect(parsedArgs.command).toBe(Command.VersionCheck); 51 | }); 52 | 53 | test('deploy to dev arg parsing works', () => { 54 | const inputArgs = ['deploy', '--target', 'dev']; 55 | const parsedArgs = parseInputParameters(inputArgs); 56 | expect(parsedArgs.command).toBe(Command.Deploy); 57 | expect(parsedArgs.target).toBe(DeployTarget.Dev); 58 | }); 59 | 60 | test('deploy to prod arg parsing works', () => { 61 | const inputArgs = ['deploy', '--target', 'prod']; 62 | const parsedArgs = parseInputParameters(inputArgs); 63 | expect(parsedArgs.command).toBe(Command.Deploy); 64 | expect(parsedArgs.target).toBe(DeployTarget.Prod); 65 | }); 66 | 67 | test('deploy to custom target fails if --config-file not set', () => { 68 | expect(() => { 69 | const inputArgs = ['deploy', '--target', 'custom']; 70 | parseInputParameters(inputArgs); 71 | }).toThrow(FakeExitError); 72 | }); 73 | 74 | test('deploy to custom target works if --config-file is set', () => { 75 | const inputArgs = [ 76 | 'deploy', 77 | '--target', 78 | 'custom', 79 | '--config-file', 80 | 'myconfig.json', 81 | ]; 82 | const parsedArgs = parseInputParameters(inputArgs); 83 | expect(parsedArgs.command).toBe(Command.Deploy); 84 | expect(parsedArgs.target).toBe(DeployTarget.Custom); 85 | }); 86 | 87 | test('deploy to custom target works if --config-file is set and --new-version is set', () => { 88 | const inputArgs = [ 89 | 'deploy', 90 | '--target', 91 | 'custom', 92 | '--config-file', 93 | 'myconfig.json', 94 | '--new-version', 95 | '1.2.3', 96 | ]; 97 | const parsedArgs = parseInputParameters(inputArgs); 98 | expect(parsedArgs.command).toBe(Command.Deploy); 99 | expect(parsedArgs.target).toBe(DeployTarget.Custom); 100 | expect(parsedArgs.newVersion).toBe('1.2.3'); 101 | }); 102 | 103 | test('fail if --config-file is set and target is not custom', () => { 104 | { 105 | expect(() => { 106 | const inputArgs = [ 107 | 'deploy', 108 | '--target', 109 | 'dev', 110 | '--config-file', 111 | 'myconfig.json', 112 | ]; 113 | parseInputParameters(inputArgs); 114 | }).toThrow(FakeExitError); 115 | } 116 | 117 | { 118 | expect(() => { 119 | const inputArgs = [ 120 | 'deploy', 121 | '--target', 122 | 'prod', 123 | '--config-file', 124 | 'myconfig.json', 125 | ]; 126 | parseInputParameters(inputArgs); 127 | }).toThrow(FakeExitError); 128 | } 129 | }); 130 | 131 | test('fail if --new-version is set and target is not custom', () => { 132 | { 133 | expect(() => { 134 | const inputArgs = ['deploy', '--target', 'dev', '--new-version', '1.2.3']; 135 | parseInputParameters(inputArgs); 136 | }).toThrow(FakeExitError); 137 | } 138 | 139 | { 140 | expect(() => { 141 | const inputArgs = [ 142 | 'deploy', 143 | '--target', 144 | 'prod', 145 | '--new-version', 146 | '1.2.3', 147 | ]; 148 | parseInputParameters(inputArgs); 149 | }).toThrow(FakeExitError); 150 | } 151 | }); 152 | -------------------------------------------------------------------------------- /ops/deploy/__tests__/test-deploy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import deploy, { 18 | ExtensionPublishArgs, 19 | getEnvValueOrPanic, 20 | VSSExtensionOverrideJson, 21 | getAzUrl, 22 | errorIfNewVersionAndTargetNotCustom, 23 | } from '../deploy'; 24 | import { DeployTarget } from '../cli-args'; 25 | 26 | test('test getEnvValueOrPanic works', () => { 27 | process.env = Object.assign(process.env, { EXTENSION_ID: 'test-value' }); 28 | const x = getEnvValueOrPanic('EXTENSION_ID'); 29 | expect(x).toBe('test-value'); 30 | 31 | delete process.env.EXTENSION_ID; 32 | 33 | expect(() => { 34 | getEnvValueOrPanic('EXTENSION_ID'); 35 | }).toThrow(Error); 36 | }); 37 | 38 | test('verify we can get the input args from env vars', () => { 39 | process.env = Object.assign(process.env, { 40 | EXTENSION_ID: 'EXTENSION_ID-TEST_VALUE', 41 | }); 42 | process.env = Object.assign(process.env, { 43 | EXTENSION_NAME: 'EXTENSION_NAME-TEST_VALUE', 44 | }); 45 | process.env = Object.assign(process.env, { TASK_ID: 'TASK_ID-TEST_VALUE' }); 46 | process.env = Object.assign(process.env, { 47 | TASK_NAME: 'TASK_NAME-TEST_VALUE', 48 | }); 49 | process.env = Object.assign(process.env, { 50 | TASK_FRIENDLY_NAME: 'TASK_FRIENDLY_NAME-TEST_VALUE', 51 | }); 52 | process.env = Object.assign(process.env, { 53 | AZURE_DEVOPS_EXT_PAT: 'AZURE_DEVOPS_EXT_PAT-TEST_VALUE', 54 | }); 55 | process.env = Object.assign(process.env, { 56 | AZURE_DEVOPS_ORG: 'AZURE_DEVOPS_ORG-TEST_VALUE', 57 | }); 58 | process.env = Object.assign(process.env, { 59 | VS_MARKETPLACE_PUBLISHER: 'VS_MARKETPLACE_PUBLISHER-TEST_VALUE', 60 | }); 61 | 62 | const x: ExtensionPublishArgs = deploy.getExtensionPublishArgsFromEnvVars(); 63 | expect(x.extensionId).toBe('EXTENSION_ID-TEST_VALUE'); 64 | expect(x.extensionName).toBe('EXTENSION_NAME-TEST_VALUE'); 65 | expect(x.taskId).toBe('TASK_ID-TEST_VALUE'); 66 | expect(x.taskName).toBe('TASK_NAME-TEST_VALUE'); 67 | expect(x.taskFriendlyName).toBe('TASK_FRIENDLY_NAME-TEST_VALUE'); 68 | expect(x.azureDevopsPAT).toBe('AZURE_DEVOPS_EXT_PAT-TEST_VALUE'); 69 | expect(x.azureOrg).toBe('AZURE_DEVOPS_ORG-TEST_VALUE'); 70 | expect(x.vsMarketplacePublisher).toBe('VS_MARKETPLACE_PUBLISHER-TEST_VALUE'); 71 | 72 | delete process.env.EXTENSION_ID; 73 | delete process.env.EXTENSION_NAME; 74 | delete process.env.TASK_ID; 75 | delete process.env.TASK_NAME; 76 | delete process.env.TASK_FRIENDLY_NAME; 77 | delete process.env.AZURE_DEVOPS_EXT_PAT; 78 | delete process.env.AZURE_DEVOPS_ORG; 79 | delete process.env.VS_MARKETPLACE_PUBLISHER; 80 | }); 81 | 82 | test('verify we can get the input args from custom config file', () => { 83 | process.env = Object.assign(process.env, { 84 | AZURE_DEVOPS_EXT_PAT: 'AZURE_DEVOPS_EXT_PAT-TEST_VALUE', 85 | }); 86 | 87 | const mockFs = require('mock-fs'); 88 | mockFs({ 89 | 'mock-config-file.json': `{ 90 | "EXTENSION_ID": "EXTENSION_ID-TEST_VALUE", 91 | "EXTENSION_NAME": "EXTENSION_NAME-TEST_VALUE", 92 | "TASK_ID": "TASK_ID-TEST_VALUE", 93 | "TASK_NAME": "TASK_NAME-TEST_VALUE", 94 | "TASK_FRIENDLY_NAME": "TASK_FRIENDLY_NAME-TEST_VALUE", 95 | "AZURE_DEVOPS_ORG": "AZURE_DEVOPS_ORG-TEST_VALUE", 96 | "VS_MARKETPLACE_PUBLISHER": "VS_MARKETPLACE_PUBLISHER-TEST_VALUE" 97 | }`, 98 | }); 99 | 100 | const x: ExtensionPublishArgs = deploy.getExtensionPublishArgsFromConfigFile( 101 | 'mock-config-file.json', 102 | ); 103 | 104 | expect(x.extensionId).toBe('EXTENSION_ID-TEST_VALUE'); 105 | expect(x.extensionName).toBe('EXTENSION_NAME-TEST_VALUE'); 106 | expect(x.taskId).toBe('TASK_ID-TEST_VALUE'); 107 | expect(x.taskName).toBe('TASK_NAME-TEST_VALUE'); 108 | expect(x.taskFriendlyName).toBe('TASK_FRIENDLY_NAME-TEST_VALUE'); 109 | expect(x.azureDevopsPAT).toBe('AZURE_DEVOPS_EXT_PAT-TEST_VALUE'); 110 | expect(x.azureOrg).toBe('AZURE_DEVOPS_ORG-TEST_VALUE'); 111 | expect(x.vsMarketplacePublisher).toBe('VS_MARKETPLACE_PUBLISHER-TEST_VALUE'); 112 | 113 | delete process.env.AZURE_DEVOPS_EXT_PAT; 114 | }); 115 | 116 | test('test override json builder', () => { 117 | const jsonStr: string = VSSExtensionOverrideJson.build() 118 | .withExtensionName('myExtensionName') 119 | .getJsonString(); 120 | const jsonObj = JSON.parse(jsonStr); 121 | expect(jsonObj.name).toBe('myExtensionName'); 122 | expect(jsonObj.public).toBe(undefined); 123 | expect(jsonObj.version).toBe(undefined); 124 | expect(jsonObj.id).toBe(undefined); 125 | expect(jsonObj.publisher).toBe(undefined); 126 | 127 | const jsonStr2: string = VSSExtensionOverrideJson.build() 128 | .withExtensionName('myExtensionName') 129 | .withPublishPublic(true) 130 | .getJsonString(); 131 | const jsonObj2 = JSON.parse(jsonStr2); 132 | expect(jsonObj2.name).toBe('myExtensionName'); 133 | expect(jsonObj2.public).toBe(true); 134 | expect(jsonObj2.version).toBe(undefined); 135 | expect(jsonObj2.id).toBe(undefined); 136 | expect(jsonObj2.publisher).toBe(undefined); 137 | 138 | const jsonStr3: string = VSSExtensionOverrideJson.build() 139 | .withExtensionName('myExtensionName') 140 | .withPublishPublic(true) 141 | .withVersion('1.2.3') 142 | .getJsonString(); 143 | const jsonObj3 = JSON.parse(jsonStr3); 144 | expect(jsonObj3.name).toBe('myExtensionName'); 145 | expect(jsonObj3.public).toBe(true); 146 | expect(jsonObj3.version).toBe('1.2.3'); 147 | expect(jsonObj3.id).toBe(undefined); 148 | expect(jsonObj3.publisher).toBe(undefined); 149 | 150 | const jsonStr4: string = VSSExtensionOverrideJson.build() 151 | .withExtensionName('myExtensionName') 152 | .withPublishPublic(true) 153 | .withVersion('1.2.3') 154 | .withExtensionId('test-extension-id') 155 | .getJsonString(); 156 | const jsonObj4 = JSON.parse(jsonStr4); 157 | expect(jsonObj4.name).toBe('myExtensionName'); 158 | expect(jsonObj4.public).toBe(true); 159 | expect(jsonObj4.version).toBe('1.2.3'); 160 | expect(jsonObj4.id).toBe('test-extension-id'); 161 | expect(jsonObj4.publisher).toBe(undefined); 162 | 163 | const jsonStr5: string = VSSExtensionOverrideJson.build() 164 | .withExtensionName('myExtensionName') 165 | .withPublishPublic(true) 166 | .withVersion('1.2.3') 167 | .withExtensionId('test-extension-id') 168 | .withExtensionPublisher('test-publisher') 169 | .getJsonString(); 170 | const jsonObj5 = JSON.parse(jsonStr5); 171 | expect(jsonObj5.name).toBe('myExtensionName'); 172 | expect(jsonObj5.public).toBe(true); 173 | expect(jsonObj5.version).toBe('1.2.3'); 174 | expect(jsonObj5.id).toBe('test-extension-id'); 175 | expect(jsonObj5.publisher).toBe('test-publisher'); 176 | }); 177 | 178 | test('test getAzUrl works', () => { 179 | expect(getAzUrl('test')).toBe('https://dev.azure.com/test/'); 180 | }); 181 | 182 | test('test errorIfNewVersionAndTargetNotCustom works', () => { 183 | errorIfNewVersionAndTargetNotCustom(DeployTarget.Custom, '0.0.1'); // should not throw error 184 | 185 | expect(() => { 186 | errorIfNewVersionAndTargetNotCustom(DeployTarget.Dev, '0.0.1'); 187 | }).toThrow(Error); 188 | 189 | expect(() => { 190 | errorIfNewVersionAndTargetNotCustom(DeployTarget.Dev, '0.0.1'); 191 | }).toThrow(Error); 192 | }); 193 | -------------------------------------------------------------------------------- /ops/deploy/__tests__/test-json-file-updater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | test('test JsonFileUpdater', () => { 18 | const deployModule = require('../deploy'); 19 | 20 | const mockFs = require('mock-fs'); 21 | mockFs({ 22 | 'mock-json-file.json': `{ 23 | "name": "test-name", 24 | "otherField": "test-otherField" 25 | }`, 26 | }); 27 | 28 | const updates = { 29 | name: 'new-name', 30 | version: { 31 | Major: 0, 32 | Minor: 0, 33 | Patch: 20, 34 | }, 35 | }; 36 | 37 | deployModule.JsonFileUpdater.build() 38 | .setJsonFile('mock-json-file.json') 39 | .withUpdates(updates) 40 | .updateFile(); 41 | 42 | const fs = require('fs'); 43 | 44 | const jsonObjAfterUpdate = JSON.parse( 45 | fs.readFileSync('mock-json-file.json', 'utf8'), 46 | ); 47 | expect(jsonObjAfterUpdate.name).toBe('new-name'); 48 | expect(jsonObjAfterUpdate.otherField).toBe('test-otherField'); 49 | }); 50 | 51 | test("JsonFileUpdater doesn't write the file out unless there are updates", () => { 52 | const mockFn = jest.fn().mockReturnValue(`{ 53 | "name": "test-name-x", 54 | "otherField": "test-otherField" 55 | }`); 56 | 57 | const mockFsWriteFileSync = jest.fn(); // no-op implementation 58 | 59 | jest.doMock('fs', () => { 60 | return { 61 | readFileSync: mockFn, 62 | writeFileSync: mockFsWriteFileSync, 63 | }; 64 | }); 65 | 66 | const deployModule = require('../deploy'); 67 | 68 | deployModule.JsonFileUpdater.build() 69 | .setJsonFile('mock-json-file.json') 70 | // no updates 71 | .updateFile(); 72 | 73 | expect(mockFsWriteFileSync).toHaveBeenCalledTimes(0); 74 | }); 75 | -------------------------------------------------------------------------------- /ops/deploy/cli-args.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const yargs = require('yargs'); 18 | 19 | enum Command { 20 | VersionCheck, 21 | Deploy, 22 | } 23 | 24 | enum DeployTarget { 25 | Prod, 26 | Dev, 27 | Custom, 28 | } 29 | 30 | interface InputArgs { 31 | command: Command; 32 | target: DeployTarget; 33 | configFile: string; 34 | newVersion: string; 35 | } 36 | 37 | const parseInputParameters = (inputArgs): InputArgs => { 38 | const scriptName = 'deploy'; 39 | const usageMsg = 'Usage: $0 '; 40 | 41 | const argv = yargs(inputArgs) 42 | .version() 43 | .scriptName(scriptName) 44 | .usage(usageMsg) 45 | .command('version-check', 'Checks that all the versions match') 46 | .command('deploy [options]', 'Deploys to specified target', (y) => { 47 | y.option('target', { 48 | description: 'the deployment target', 49 | type: 'string', 50 | demandOption: true, 51 | choices: ['dev', 'prod', 'custom'], 52 | }) 53 | .option('config-file', { 54 | description: 55 | 'Config JSON file specifying custom deployment arguments', 56 | type: 'string', 57 | }) 58 | .option('new-version', { 59 | description: 60 | 'New version to release; only valid with --target=custom', 61 | type: 'string', 62 | }) 63 | .check((argsToCheck) => { 64 | if (argsToCheck.target === 'custom' && !argsToCheck.configFile) { 65 | throw new Error('--config-file is required for target `custom`'); 66 | } 67 | 68 | if (argsToCheck.target !== 'custom' && argsToCheck.configFile) { 69 | throw new Error( 70 | '--config-file isonly allowed with target `custom`', 71 | ); 72 | } 73 | 74 | if (argsToCheck.target !== 'custom' && argsToCheck.newVersion) { 75 | throw new Error( 76 | '--new-version is only allowed with target `custom`', 77 | ); 78 | } 79 | 80 | return true; 81 | }); 82 | }) 83 | .demandCommand(1) 84 | .help('help') 85 | .alias('help', 'h') 86 | .example('$0 version-check') 87 | .example('$0 deploy --target=dev') 88 | .example('$0 deploy --target=prod') 89 | .example('$0 deploy --target=custom --config-file=custom-args.json').argv; 90 | 91 | console.log('argv:', argv); 92 | 93 | return parseOptions(argv); 94 | }; 95 | 96 | const parseOptions = (argv: any): InputArgs => { 97 | const options = { 98 | command: Command.VersionCheck, 99 | target: DeployTarget.Custom, 100 | configFile: '', 101 | newVersion: '', 102 | } as InputArgs; 103 | 104 | const command = argv._[0]; 105 | if (command === 'version-check') { 106 | options.command = Command.VersionCheck; 107 | } else if (command === 'deploy') { 108 | options.command = Command.Deploy; 109 | 110 | if (argv.target) { 111 | if (argv.target === 'dev') { 112 | options.target = DeployTarget.Dev; 113 | } else if (argv.target === 'prod') { 114 | options.target = DeployTarget.Prod; 115 | } else if (argv.target === 'custom') { 116 | options.target = DeployTarget.Custom; 117 | if (argv.configFile) { 118 | options.configFile = argv.configFile; 119 | } 120 | if (argv.newVersion) { 121 | options.newVersion = argv.newVersion; 122 | } 123 | } else { 124 | // this should never happen 125 | throw new Error(`Invalid command: ${command}`); 126 | } 127 | } 128 | } else { 129 | // this should never happen 130 | throw new Error(`Invalid deploy target: ${argv.target}`); 131 | } 132 | 133 | return options; 134 | }; 135 | 136 | export { InputArgs, parseInputParameters, Command, DeployTarget }; 137 | -------------------------------------------------------------------------------- /ops/deploy/get-current-dev-ext-version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { getExtensionInfo, getLatestVersion } from './lib/azure-devops'; 18 | 19 | async function main() { 20 | const publisherName = process.env.DEV_AZ_PUBLISHER || ''; 21 | // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name 22 | const extensionName = process.env.DEV_AZ_EXTENSION_ID || ''; 23 | const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ''; 24 | 25 | const extensionDetails = await getExtensionInfo( 26 | azToken, 27 | publisherName, 28 | extensionName, 29 | ); 30 | 31 | if (extensionDetails) { 32 | const latestVersion = getLatestVersion(extensionDetails); 33 | if (latestVersion) { 34 | console.log(latestVersion); 35 | } else { 36 | console.error('could not get extension version info'); 37 | process.exit(1); 38 | } 39 | } else { 40 | console.error('could not get extension info'); 41 | process.exit(1); 42 | } 43 | } 44 | 45 | if (require.main === module) { 46 | main(); 47 | } 48 | -------------------------------------------------------------------------------- /ops/deploy/get-next-dev-ext-version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { getExtensionInfo, getLatestVersion } from './lib/azure-devops'; 18 | 19 | async function main() { 20 | const publisherName = process.env.DEV_AZ_PUBLISHER || ''; 21 | // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name 22 | const extensionName = process.env.DEV_AZ_EXTENSION_ID || ''; 23 | const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ''; 24 | 25 | const extensionDetails = await getExtensionInfo( 26 | azToken, 27 | publisherName, 28 | extensionName, 29 | ); 30 | 31 | if (extensionDetails) { 32 | const latestVersion = getLatestVersion(extensionDetails); 33 | if (latestVersion) { 34 | const splitz = latestVersion.split('.'); 35 | const major = parseInt(splitz[0]); 36 | const minor = parseInt(splitz[1]); 37 | const patch = parseInt(splitz[2]) + 1; 38 | const newVersion = `${major}.${minor}.${patch}`; 39 | console.log(newVersion); 40 | } else { 41 | console.error('could not get extension version info'); 42 | process.exit(1); 43 | } 44 | } else { 45 | // Could not get extension info. The extension must not exist. Returning `0.0.1` for first version of new extension. 46 | console.log('0.0.1'); 47 | } 48 | } 49 | 50 | if (require.main === module) { 51 | main(); 52 | } 53 | -------------------------------------------------------------------------------- /ops/deploy/install-extension-to-dev-org.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WebApi } from 'azure-devops-node-api'; 18 | 19 | import { getAzUrl, getWebApi } from './lib/azure-devops'; 20 | import { 21 | installExtension, 22 | getInstalledExtensionInfo, 23 | uninstallExtension, 24 | } from './lib/azure-devops/extensions'; 25 | import { asyncSleep } from './lib/sleep'; 26 | 27 | async function main() { 28 | const publisherName = process.env.DEV_AZ_PUBLISHER || ''; 29 | // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name 30 | const extensionName = process.env.DEV_AZ_EXTENSION_ID || ''; 31 | const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ''; 32 | const azOrg = process.env.DEV_AZ_ORG || ''; 33 | 34 | const version = process.argv[2]; 35 | if (version) { 36 | console.log(`going to try to install extension version: ${version}`); 37 | } else { 38 | console.log('version must be passed in'); 39 | process.exit(1); 40 | } 41 | 42 | const azUrl = getAzUrl(azOrg); 43 | const webApi: WebApi = await getWebApi(azUrl, azToken); 44 | 45 | try { 46 | const alreadyInstalledExtensionInfo = await getInstalledExtensionInfo( 47 | webApi, 48 | publisherName, 49 | extensionName, 50 | ); 51 | 52 | if (alreadyInstalledExtensionInfo) { 53 | const alreadyInstalledVersion = alreadyInstalledExtensionInfo.version; 54 | console.log( 55 | `Extension version currently installed: ${alreadyInstalledVersion}`, 56 | ); 57 | 58 | console.log(`Uninstalling previously installed extension`); 59 | await uninstallExtension(webApi, publisherName, extensionName); 60 | } 61 | 62 | console.log( 63 | 'Attempting to install latest version of extension into org...', 64 | ); 65 | // installExtension will throw an error if it is already installed 66 | await installExtension(webApi, publisherName, extensionName, version); 67 | 68 | const afterInstallExtensionInfo = await getInstalledExtensionInfo( 69 | webApi, 70 | publisherName, 71 | extensionName, 72 | ); 73 | const afterInstallExtensionVersion = afterInstallExtensionInfo.version; 74 | console.log(`Extension version installed: ${afterInstallExtensionVersion}`); 75 | 76 | // there seems to be a delay between when the API indicates the extension is available and when a Pipeline 77 | // can be successfully launched that uses it. 78 | // so, we will sleep for 30 seconds to give it some time to sort itself out 79 | console.log( 80 | 'sleeping for 30 seconds to give Azure DevOps time to settle extension availability', 81 | ); 82 | await asyncSleep(30000); 83 | console.log('done sleeping for 30 seconds'); 84 | } catch (err) { 85 | console.log(`err.statusCode: ${err.statusCode}`); 86 | console.log(`err.message: ${err.message}`); 87 | throw err; 88 | } 89 | } 90 | 91 | if (require.main === module) { 92 | main(); 93 | } 94 | -------------------------------------------------------------------------------- /ops/deploy/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | roots: ['/'], 19 | transform: { 20 | '^.+\\.tsx?$': 'ts-jest', 21 | }, 22 | testPathIgnorePatterns: ['/node_modules/'], 23 | }; 24 | -------------------------------------------------------------------------------- /ops/deploy/lib/azure-devops/builds.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as nodeApi from 'azure-devops-node-api'; 18 | import * as BuildInterfaces from 'azure-devops-node-api/interfaces/BuildInterfaces'; 19 | 20 | export async function getBuildDefinitions( 21 | webApi: nodeApi.WebApi, 22 | projectName: string, 23 | ) { 24 | const buildApi = await webApi.getBuildApi(); 25 | const buildDefinitions = await buildApi.getDefinitions(projectName); 26 | return buildDefinitions; 27 | } 28 | 29 | export async function getBuildDefinition( 30 | webApi: nodeApi.WebApi, 31 | projectName: string, 32 | buildDefinitionId: number, 33 | ) { 34 | const buildApi = await webApi.getBuildApi(); 35 | const buildDefinition = await buildApi.getDefinition( 36 | projectName, 37 | buildDefinitionId, 38 | ); 39 | return buildDefinition; 40 | } 41 | 42 | export async function getBuilds( 43 | webApi: nodeApi.WebApi, 44 | projectName: string, 45 | buildDefinitionId, 46 | ) { 47 | const buildApi = await webApi.getBuildApi(); 48 | const buildDefinition = await buildApi.getBuilds(projectName, [ 49 | buildDefinitionId, 50 | ]); 51 | return buildDefinition; 52 | } 53 | 54 | export async function getBuild( 55 | webApi: nodeApi.WebApi, 56 | projectName: string, 57 | buildId: number, 58 | ) { 59 | const buildApi = await webApi.getBuildApi(); 60 | const buildDefinition = await buildApi.getBuild(projectName, buildId); 61 | return buildDefinition; 62 | } 63 | 64 | export async function queueBuild( 65 | webApi: nodeApi.WebApi, 66 | projectName: string, 67 | build: BuildInterfaces.Build, 68 | ) { 69 | const buildApi = await webApi.getBuildApi(); 70 | const queueBuildResult: BuildInterfaces.Build = await buildApi.queueBuild( 71 | build, 72 | projectName, 73 | ); 74 | return queueBuildResult; 75 | } 76 | 77 | export async function launchBuildPipeline( 78 | webApi: nodeApi.WebApi, 79 | azOrg: string, 80 | projectName: string, 81 | buildDefinitionId: number, 82 | ) { 83 | const apiUrl = `https://dev.azure.com/${azOrg}/${projectName}/_apis/build/builds?api-version=5.1`; 84 | const res = await webApi.rest.create(apiUrl, { 85 | definition: { 86 | id: buildDefinitionId, 87 | }, 88 | }); 89 | return res as any; 90 | } 91 | -------------------------------------------------------------------------------- /ops/deploy/lib/azure-devops/extensions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WebApi } from 'azure-devops-node-api'; 18 | import * as ExtensionManagementApi from 'azure-devops-node-api/ExtensionManagementApi'; 19 | import * as ExtensionManagementInterfaces from 'azure-devops-node-api/interfaces/ExtensionManagementInterfaces'; 20 | 21 | export async function getInstalledExtensionInfo( 22 | webApi: WebApi, 23 | publisherName: string, 24 | extensionId: string, 25 | ): Promise { 26 | const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = 27 | await webApi.getExtensionManagementApi(); 28 | 29 | // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI 30 | // Will throw error if already installed 31 | return extensionManagementApiObject.getInstalledExtensionByName( 32 | publisherName, 33 | extensionId, 34 | ); 35 | } 36 | 37 | export async function uninstallExtension( 38 | webApi: WebApi, 39 | publisherName: string, 40 | extensionId: string, 41 | ): Promise { 42 | const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = 43 | await webApi.getExtensionManagementApi(); 44 | 45 | // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI 46 | return extensionManagementApiObject.uninstallExtensionByName( 47 | publisherName, 48 | extensionId, 49 | ); 50 | } 51 | 52 | export async function installExtension( 53 | webApi: WebApi, 54 | publisherName: string, 55 | extensionId: string, 56 | version?: string, 57 | ): Promise { 58 | const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = 59 | await webApi.getExtensionManagementApi(); 60 | 61 | // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI 62 | return extensionManagementApiObject.installExtensionByName( 63 | publisherName, 64 | extensionId, 65 | version, 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /ops/deploy/lib/azure-devops/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as nodeApi from 'azure-devops-node-api'; 18 | import * as CoreApi from 'azure-devops-node-api/CoreApi'; 19 | import { GalleryApi } from 'azure-devops-node-api/GalleryApi'; 20 | import * as CoreInterfaces from 'azure-devops-node-api/interfaces/CoreInterfaces'; 21 | import * as GalleryInterfaces from 'azure-devops-node-api/interfaces/GalleryInterfaces'; 22 | import * as lim from 'azure-devops-node-api/interfaces/LocationsInterfaces'; 23 | import { WebApi, getBasicHandler } from 'azure-devops-node-api/WebApi'; 24 | 25 | export function getAzUrl(azureOrg: string): string { 26 | return `https://dev.azure.com/${azureOrg}/`; 27 | } 28 | 29 | export async function getApi( 30 | serverUrl: string, 31 | azureDevOpsToken: string, 32 | ): Promise { 33 | const token = azureDevOpsToken; 34 | const authHandler = nodeApi.getPersonalAccessTokenHandler(token); 35 | const option = undefined; 36 | 37 | const vsts: nodeApi.WebApi = new nodeApi.WebApi( 38 | serverUrl, 39 | authHandler, 40 | option, 41 | ); 42 | const connData: lim.ConnectionData = await vsts.connect(); 43 | if (!connData?.authenticatedUser) { 44 | console.error('failed to connect'); 45 | } 46 | 47 | return vsts; 48 | } 49 | 50 | export async function getWebApi( 51 | serverUrl: string, 52 | azureDevOpsToken: string, 53 | ): Promise { 54 | return await getApi(serverUrl, azureDevOpsToken); 55 | } 56 | 57 | export async function getProject(webApi: nodeApi.WebApi, projectName: string) { 58 | const coreApiObject: CoreApi.CoreApi = await webApi.getCoreApi(); 59 | const project: CoreInterfaces.TeamProject = await coreApiObject.getProject( 60 | projectName, 61 | ); 62 | console.log(project); 63 | return project; 64 | } 65 | 66 | export async function getProjects(webApi: nodeApi.WebApi) { 67 | const coreApiObject: CoreApi.CoreApi = await webApi.getCoreApi(); 68 | const projects = await coreApiObject.getProjects(); 69 | console.log(projects); 70 | return projects; 71 | } 72 | 73 | // ********************************************************** 74 | // hack / partially from tfs-cli app/exec/extension/default.ts 75 | export async function getGalleryApi(webApi: nodeApi.WebApi, azToken: string) { 76 | const handler = await getCredentials(azToken); 77 | return new GalleryApi(webApi.serverUrl, [handler]); 78 | } 79 | 80 | export async function getCredentials(azToken) { 81 | return getBasicHandler('OAuth', azToken); 82 | } 83 | // ********************************************************** 84 | 85 | export async function getExtensionInfo( 86 | azToken: string, 87 | publisherName: string, 88 | extensionName: string, 89 | ) { 90 | // This is a hack based on some fun with the MS GalleryAPI 91 | // see how we're not using the usual base API URL which includes the org - this is a generic one (but we still authenticate with our token) 92 | const webApi = await getApi('https://marketplace.visualstudio.com/', azToken); 93 | const galleryApi = await getGalleryApi(webApi, azToken); 94 | 95 | const version = undefined; 96 | 97 | // This API is the one used by tfx-cli when you call `tfx extension show`. 98 | // See https://github.com/microsoft/tfs-cli/blob/master/app/exec/extension/_lib/publish.ts#L172 99 | const extensionInfo = await galleryApi.getExtension( 100 | null, 101 | publisherName, 102 | extensionName, 103 | version, 104 | GalleryInterfaces.ExtensionQueryFlags.IncludeVersions | 105 | GalleryInterfaces.ExtensionQueryFlags.IncludeFiles | 106 | GalleryInterfaces.ExtensionQueryFlags.IncludeCategoryAndTags | 107 | GalleryInterfaces.ExtensionQueryFlags.IncludeSharedAccounts, 108 | ); 109 | return extensionInfo; 110 | } 111 | 112 | export function getLatestVersion(extensionDetails: any): string | undefined { 113 | return extensionDetails?.versions[0]?.version; 114 | } 115 | -------------------------------------------------------------------------------- /ops/deploy/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export async function asyncSleep(milliseconds: number): Promise { 18 | return new Promise((resolve) => { 19 | setTimeout(resolve, milliseconds); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /ops/deploy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "azure-devops-node-api": "^10.1.1" 9 | } 10 | }, 11 | "node_modules/azure-devops-node-api": { 12 | "version": "10.2.2", 13 | "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-10.2.2.tgz", 14 | "integrity": "sha512-4TVv2X7oNStT0vLaEfExmy3J4/CzfuXolEcQl/BRUmvGySqKStTG2O55/hUQ0kM7UJlZBLgniM0SBq4d/WkKow==", 15 | "dependencies": { 16 | "tunnel": "0.0.6", 17 | "typed-rest-client": "^1.8.4" 18 | } 19 | }, 20 | "node_modules/call-bind": { 21 | "version": "1.0.2", 22 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 23 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 24 | "dependencies": { 25 | "function-bind": "^1.1.1", 26 | "get-intrinsic": "^1.0.2" 27 | }, 28 | "funding": { 29 | "url": "https://github.com/sponsors/ljharb" 30 | } 31 | }, 32 | "node_modules/function-bind": { 33 | "version": "1.1.1", 34 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 35 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 36 | }, 37 | "node_modules/get-intrinsic": { 38 | "version": "1.2.1", 39 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 40 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 41 | "dependencies": { 42 | "function-bind": "^1.1.1", 43 | "has": "^1.0.3", 44 | "has-proto": "^1.0.1", 45 | "has-symbols": "^1.0.3" 46 | }, 47 | "funding": { 48 | "url": "https://github.com/sponsors/ljharb" 49 | } 50 | }, 51 | "node_modules/has": { 52 | "version": "1.0.3", 53 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 54 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 55 | "dependencies": { 56 | "function-bind": "^1.1.1" 57 | }, 58 | "engines": { 59 | "node": ">= 0.4.0" 60 | } 61 | }, 62 | "node_modules/has-proto": { 63 | "version": "1.0.1", 64 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 65 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 66 | "engines": { 67 | "node": ">= 0.4" 68 | }, 69 | "funding": { 70 | "url": "https://github.com/sponsors/ljharb" 71 | } 72 | }, 73 | "node_modules/has-symbols": { 74 | "version": "1.0.3", 75 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 76 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 77 | "engines": { 78 | "node": ">= 0.4" 79 | }, 80 | "funding": { 81 | "url": "https://github.com/sponsors/ljharb" 82 | } 83 | }, 84 | "node_modules/object-inspect": { 85 | "version": "1.12.3", 86 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 87 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 88 | "funding": { 89 | "url": "https://github.com/sponsors/ljharb" 90 | } 91 | }, 92 | "node_modules/qs": { 93 | "version": "6.11.2", 94 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 95 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 96 | "dependencies": { 97 | "side-channel": "^1.0.4" 98 | }, 99 | "engines": { 100 | "node": ">=0.6" 101 | }, 102 | "funding": { 103 | "url": "https://github.com/sponsors/ljharb" 104 | } 105 | }, 106 | "node_modules/side-channel": { 107 | "version": "1.0.4", 108 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 109 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 110 | "dependencies": { 111 | "call-bind": "^1.0.0", 112 | "get-intrinsic": "^1.0.2", 113 | "object-inspect": "^1.9.0" 114 | }, 115 | "funding": { 116 | "url": "https://github.com/sponsors/ljharb" 117 | } 118 | }, 119 | "node_modules/tunnel": { 120 | "version": "0.0.6", 121 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 122 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 123 | "engines": { 124 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 125 | } 126 | }, 127 | "node_modules/typed-rest-client": { 128 | "version": "1.8.11", 129 | "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", 130 | "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", 131 | "dependencies": { 132 | "qs": "^6.9.1", 133 | "tunnel": "0.0.6", 134 | "underscore": "^1.12.1" 135 | } 136 | }, 137 | "node_modules/underscore": { 138 | "version": "1.13.6", 139 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", 140 | "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" 141 | } 142 | }, 143 | "dependencies": { 144 | "azure-devops-node-api": { 145 | "version": "10.2.2", 146 | "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-10.2.2.tgz", 147 | "integrity": "sha512-4TVv2X7oNStT0vLaEfExmy3J4/CzfuXolEcQl/BRUmvGySqKStTG2O55/hUQ0kM7UJlZBLgniM0SBq4d/WkKow==", 148 | "requires": { 149 | "tunnel": "0.0.6", 150 | "typed-rest-client": "^1.8.4" 151 | } 152 | }, 153 | "call-bind": { 154 | "version": "1.0.2", 155 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 156 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 157 | "requires": { 158 | "function-bind": "^1.1.1", 159 | "get-intrinsic": "^1.0.2" 160 | } 161 | }, 162 | "function-bind": { 163 | "version": "1.1.1", 164 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 165 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 166 | }, 167 | "get-intrinsic": { 168 | "version": "1.2.1", 169 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 170 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 171 | "requires": { 172 | "function-bind": "^1.1.1", 173 | "has": "^1.0.3", 174 | "has-proto": "^1.0.1", 175 | "has-symbols": "^1.0.3" 176 | } 177 | }, 178 | "has": { 179 | "version": "1.0.3", 180 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 181 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 182 | "requires": { 183 | "function-bind": "^1.1.1" 184 | } 185 | }, 186 | "has-proto": { 187 | "version": "1.0.1", 188 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 189 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" 190 | }, 191 | "has-symbols": { 192 | "version": "1.0.3", 193 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 194 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 195 | }, 196 | "object-inspect": { 197 | "version": "1.12.3", 198 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 199 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" 200 | }, 201 | "qs": { 202 | "version": "6.11.2", 203 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 204 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 205 | "requires": { 206 | "side-channel": "^1.0.4" 207 | } 208 | }, 209 | "side-channel": { 210 | "version": "1.0.4", 211 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 212 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 213 | "requires": { 214 | "call-bind": "^1.0.0", 215 | "get-intrinsic": "^1.0.2", 216 | "object-inspect": "^1.9.0" 217 | } 218 | }, 219 | "tunnel": { 220 | "version": "0.0.6", 221 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 222 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" 223 | }, 224 | "typed-rest-client": { 225 | "version": "1.8.11", 226 | "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", 227 | "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", 228 | "requires": { 229 | "qs": "^6.9.1", 230 | "tunnel": "0.0.6", 231 | "underscore": "^1.12.1" 232 | } 233 | }, 234 | "underscore": { 235 | "version": "1.13.6", 236 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", 237 | "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /ops/deploy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "azure-devops-node-api": "^10.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ops/deploy/run-test-pipelines.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WebApi } from 'azure-devops-node-api'; 18 | import * as fs from 'fs'; 19 | 20 | import { getAzUrl, getWebApi } from './lib/azure-devops'; 21 | 22 | import { getBuild, launchBuildPipeline } from './lib/azure-devops/builds'; 23 | 24 | import { 25 | BuildStatus, 26 | BuildResult, 27 | } from 'azure-devops-node-api/interfaces/BuildInterfaces'; 28 | 29 | interface AzureVars { 30 | azOrg: string; 31 | azureDevopsExtPAT: string; 32 | } 33 | 34 | function getEnvVars(): AzureVars { 35 | const azOrg = process.env.DEV_AZ_ORG || ''; 36 | const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ''; 37 | 38 | const vars: AzureVars = { 39 | azOrg: azOrg, 40 | azureDevopsExtPAT: azToken, 41 | }; 42 | return vars; 43 | } 44 | 45 | async function main() { 46 | console.log(`pwd: ${process.cwd()}`); 47 | 48 | const testBuildConfigFileStr = fs.readFileSync( 49 | './ops/deploy/test-builds.json', 50 | 'utf8', 51 | ); 52 | const testBuildDefinitions = JSON.parse(testBuildConfigFileStr); 53 | 54 | const azVars = getEnvVars(); 55 | const azUrl = getAzUrl(azVars.azOrg); 56 | 57 | const webApi: WebApi = await getWebApi(azUrl, azVars.azureDevopsExtPAT); 58 | 59 | const allBuilds: Promise[] = []; 60 | 61 | for (const nextTestBuildDefinition of testBuildDefinitions) { 62 | const testProjectName = nextTestBuildDefinition.projectName; 63 | const testBuildDefinitionId = nextTestBuildDefinition.buildDefinitionId; 64 | 65 | const buildPromise = runBuild( 66 | webApi, 67 | azVars.azOrg, 68 | testProjectName, 69 | testBuildDefinitionId, 70 | ); 71 | 72 | allBuilds.push(buildPromise); 73 | } 74 | 75 | console.log('waiting for all builds to complete'); 76 | try { 77 | await Promise.all(allBuilds); 78 | console.log('all builds complete'); 79 | } catch (errAll) { 80 | console.log('error awaiting all builds'); 81 | console.log(errAll); 82 | process.exit(1); 83 | } 84 | } 85 | 86 | /** 87 | * 88 | * @returns Promise such that the bool is true if the build ran succesfully without failures and false if it ran but had failures. Should reject the promise on error. 89 | */ 90 | async function runBuild( 91 | webApi: WebApi, 92 | azOrg: string, 93 | testProjectName: string, 94 | testBuildDefinitionId: number, 95 | ): Promise { 96 | let success = false; 97 | 98 | console.log( 99 | `Starting build for project: ${testProjectName} with build definition ID: ${testBuildDefinitionId}`, 100 | ); 101 | 102 | try { 103 | const launchPipelineResult = await launchBuildPipeline( 104 | webApi, 105 | azOrg, 106 | testProjectName, 107 | testBuildDefinitionId, 108 | ); 109 | 110 | const buildId = launchPipelineResult.result.id; 111 | 112 | const alwaysBeTrue = 1 === 1; 113 | while (alwaysBeTrue) { 114 | const checkBuildStatusRes = await getBuild( 115 | webApi, 116 | testProjectName, 117 | buildId, 118 | ); 119 | 120 | const status = checkBuildStatusRes.status; 121 | 122 | console.log(`status: ${status}`); 123 | console.log(`BuildStatus.Completed: ${BuildStatus.Completed}`); 124 | 125 | if (!status) { 126 | throw new Error('status is not set'); 127 | } 128 | 129 | if (status === BuildStatus.Completed) { 130 | console.log(`build is complete for ${testProjectName}`); 131 | const result = checkBuildStatusRes.result; 132 | console.log(`build result: ${result}`); 133 | if (result) { 134 | if (result === BuildResult.Succeeded) { 135 | console.log(`build succeeded for ${testProjectName}`); 136 | success = true; 137 | } else { 138 | console.log( 139 | `build did not succeed for ${testProjectName}. BuildResult code: ${result}`, 140 | ); 141 | } 142 | } 143 | break; 144 | } else { 145 | console.log( 146 | `Still waiting for build ${buildId} (${testProjectName}) to complete. Status: ${status}. Time: ${new Date().getTime()}`, 147 | ); 148 | await asyncSleep(10000); 149 | } 150 | } 151 | 152 | if (success) { 153 | return Promise.resolve(); 154 | } else { 155 | console.log('resolving false - not successful'); 156 | return Promise.reject(); 157 | } 158 | } catch (err) { 159 | console.log( 160 | `Failed to launch/check build for project: ${testProjectName} with build definition ID: ${testBuildDefinitionId}`, 161 | ); 162 | console.log(err); 163 | console.log('\nrejecting - not successful'); 164 | return Promise.reject(); 165 | } 166 | } 167 | 168 | async function asyncSleep(milliseconds: number): Promise { 169 | return new Promise((resolve) => { 170 | setTimeout(resolve, milliseconds); 171 | }); 172 | } 173 | 174 | if (require.main === module) { 175 | main(); 176 | } 177 | -------------------------------------------------------------------------------- /ops/deploy/test-builds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "projectName": "goof", 4 | "buildDefinitionId": 3 5 | }, 6 | { 7 | "projectName": "goof_windows", 8 | "buildDefinitionId": 4 9 | }, 10 | { 11 | "projectName": "azure-maven-multimodule-goof", 12 | "buildDefinitionId": 5 13 | }, 14 | { 15 | "projectName": "snyk-6598", 16 | "buildDefinitionId": 6 17 | }, 18 | { 19 | "projectName": "DotNetFrameworkSampleCLIApp", 20 | "buildDefinitionId": 7 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /ops/deploy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // revert to es6 /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "./dist" /* Redirect output structure to the directory. */, 6 | 7 | /* Strict Type-Checking Options */ 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 10 | "skipLibCheck": true, 11 | 12 | /* Module Resolution Options */ 13 | // "types": [ /* Type declaration files to be included in compilation. */ 14 | // "vss-web-extension-sdk", 15 | // "jquery" 16 | // ], 17 | 18 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 19 | 20 | /* Source Map Options */ 21 | "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */, 22 | "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */, 23 | "useUnknownInCatchVariables": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snyk-azure-pipelines-task", 3 | "version": "0.2.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "install_dependencies": "npm install && cd snykTask && npm install && cd .. && cd ops/deploy && npm install", 8 | "build": "npm run install_dependencies && npm run compile && npm run enhancer:compile && npm run deploy:compile", 9 | "build:clean": "npm run clean:install_dependencies && npm run compile && npm run enhancer:compile && npm run deploy:compile", 10 | "clean:install_dependencies": "npm ci && cd snykTask && npm ci && cd .. && cd ops/deploy && npm ci", 11 | "pretest": "npm run build", 12 | "test": "npm run test:checks && npm run test:unit", 13 | "test:checks": "npm run eslint && npm run format:check", 14 | "test:snyk": "npx snyk test --severity-threshold=high", 15 | "test:unit": "jest", 16 | "compile": "tsc -b snykTask/tsconfig.json", 17 | "eslint": "eslint --cache 'snykTask/{src,test,public/js/{!(build)}}/**/*.{js,ts}'", 18 | "format:check": "prettier --check '**/*.{js,ts,json,yaml,yml,md,html}'", 19 | "format": "prettier --write '**/*.{js,ts,json,yaml,yml,md,html}'", 20 | "enhancer:compile": "tsc -b ui/enhancer/tsconfig.json", 21 | "deploy:compile": "tsc -b ops/deploy/tsconfig.json", 22 | "deploy:test": "jest --config=ops/deploy/jest.config.js", 23 | "deploy:eslint": "eslint --cache 'ops/deploy/**/*.ts'", 24 | "deploy:run": "node ops/deploy/dist/deploy.js" 25 | }, 26 | "author": "snyk.io", 27 | "license": "Apache-2.0", 28 | "dependencies": { 29 | "azure-pipelines-task-lib": "^4.13.0", 30 | "jquery": "^3.4.1", 31 | "semver": "^7.6.0", 32 | "vss-web-extension-sdk": "^5.141.0" 33 | }, 34 | "devDependencies": { 35 | "@semantic-release/changelog": "^5.0.1", 36 | "@semantic-release/exec": "^5.0.0", 37 | "@semantic-release/git": "^9.0.1", 38 | "@types/jest": "^27.0.0", 39 | "@types/node": "^16.11.10", 40 | "@types/q": "^1.5.2", 41 | "@typescript-eslint/eslint-plugin": "^2.0.0", 42 | "@typescript-eslint/parser": "^2.0.0", 43 | "eslint": "^6.2.2", 44 | "eslint-config-prettier": "^6.1.0", 45 | "eslint-plugin-import": "^2.18.2", 46 | "eslint-plugin-jest": "^22.15.2", 47 | "fs-extra": "^9.1.0", 48 | "jest": "^28.0.0", 49 | "mock-fs": "^4.10.4", 50 | "nock": "^13.5.4", 51 | "prettier": "^2.3.1", 52 | "semantic-release": "^17.0.4", 53 | "tfx-cli": "^0.7.11", 54 | "ts-jest": "^28.0.8", 55 | "typescript": "^5.1.6", 56 | "uuid": "^9.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/ci-build.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 Snyk Ltd. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # This script build the project 18 | # $1 - env (default not prod) 19 | 20 | env="test" 21 | if [[ ! -z "$1" ]]; then 22 | env="$1" 23 | fi 24 | echo "env: ${env}" 25 | 26 | # `npm run build` now being done in CI 27 | 28 | if [[ $env == "prod" ]]; then 29 | echo "npm prune --production..." 30 | npm prune --production # remove devDependencies from node-modules 31 | fi 32 | echo "Project built" 33 | -------------------------------------------------------------------------------- /scripts/ci-deploy-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 Snyk Ltd. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This script deploys a dev version of the extension to the dev environment for development / testing purposes. 20 | # It also shares / installs it into the given Azure organization. 21 | # Arguments: 22 | # $1 - Extension version 23 | # $2 - Organization that will be shared the extension 24 | 25 | # Handle arguments 26 | INPUT_PARAM_AZ_EXT_NEW_VERSION="$1" 27 | INPUT_PARAM_AZ_ORG="$2" 28 | 29 | # Check if the Azure CLI is already installed. If not, install it. 30 | az -v >/dev/null 2>&1 31 | if [[ ! $? -eq 0 ]]; then 32 | echo "Installing AZ Cli..." 33 | platform=$OSTYPE 34 | echo "Platform: ${platform}" 35 | if [[ $platform == "linux-gnu" ]]; then 36 | curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash 37 | az extension add --name azure-devops 38 | elif [[ $platform == "darwin"* ]]; then 39 | brew -v >/dev/null 2>&1 40 | if [[ $? -eq 0 ]]; then 41 | brew update && brew install azure-cli 42 | else 43 | echo "You need to have brew or install AZ Cli manually" 44 | exit 1 45 | fi 46 | else 47 | echo "Platform ${platform} not supported" 48 | exit 1 49 | fi 50 | fi 51 | 52 | # echo "See if the extension is installed..." 53 | az devops extension show \ 54 | --publisher-name $DEV_AZ_PUBLISHER \ 55 | --extension-name $DEV_AZ_EXTENSION_ID \ 56 | --organization "https://dev.azure.com/${INPUT_PARAM_AZ_ORG}/" 57 | 58 | if [[ $? -eq 0 ]]; then 59 | echo "Extension is installed in org ${INPUT_PARAM_AZ_ORG}... uninstall it" 60 | # Unistall the extinsion if it has been already installed in this organization. 61 | # This may not be required it works much more consistently with it. 62 | echo "Uninstall extension..." 63 | az devops extension uninstall \ 64 | --publisher-name $DEV_AZ_PUBLISHER \ 65 | --extension-name $DEV_AZ_EXTENSION_ID \ 66 | --organization "https://dev.azure.com/${INPUT_PARAM_AZ_ORG}/" --yes 67 | echo "Extension uninstalled" 68 | else 69 | echo "Extension not already installed." 70 | fi 71 | 72 | echo "About to deploy to dev environment using:" 73 | echo "INPUT_PARAM_AZ_EXT_NEW_VERSION: ${INPUT_PARAM_AZ_EXT_NEW_VERSION}" 74 | echo "DEV_AZ_PUBLISHER: ${DEV_AZ_PUBLISHER}" 75 | echo "DEV_AZ_EXTENSION_ID: ${DEV_AZ_EXTENSION_ID}" 76 | echo "DEV_AZ_TASK_NAME: ${DEV_AZ_TASK_NAME}" 77 | echo "DEV_AZ_TASK_FRIENDLY_NAME: ${DEV_AZ_TASK_FRIENDLY_NAME}" 78 | echo "INPUT_PARAM_AZ_ORG: ${INPUT_PARAM_AZ_ORG}" 79 | echo "DEV_AZ_TASK_ID: ${DEV_AZ_TASK_ID}" 80 | 81 | # Updating version in task.json file 82 | node "${PWD}/scripts/update-task-json-dev.js" ${INPUT_PARAM_AZ_EXT_NEW_VERSION} 83 | 84 | # Override version 85 | OVERRIDE_JSON="{ \"name\": \"${DEV_AZ_EXTENSION_NAME}\", \"version\": \"${INPUT_PARAM_AZ_EXT_NEW_VERSION}\" }" 86 | 87 | # See if the snykTask/dist and snykTask/node_modules folders are present 88 | echo "Checking for snykTask/dist folder..." 89 | ls -la snykTask/dist 90 | echo "checking snykTask/node_modules..." 91 | ls -la snykTask/node_modules 92 | 93 | # Publishing and sharing extension 94 | echo "Publishing and sharing extension..." 95 | echo "OVERRIDE_JSON: ${OVERRIDE_JSON}" 96 | echo "About to call \`tfx extension publish...\`" 97 | 98 | tfx extension publish --manifest-globs vss-extension-dev.json \ 99 | --version $INPUT_PARAM_AZ_EXT_NEW_VERSION \ 100 | --share-with $INPUT_PARAM_AZ_ORG \ 101 | --extension-id $DEV_AZ_EXTENSION_ID \ 102 | --publisher $DEV_AZ_PUBLISHER \ 103 | --override $OVERRIDE_JSON \ 104 | --token $DEV_AZURE_DEVOPS_EXT_PAT 105 | 106 | publish_exit_code=$? 107 | if [[ publish_exit_code -eq 0 ]]; then 108 | echo "Extension published and shared with Azure org" 109 | else 110 | echo "Extension failed to pubish with exit code ${publish_exit_code}" 111 | exit ${publish_exit_code} 112 | fi 113 | 114 | # re-install all dependencies. The dev deps were pruned off in ci-build.sh 115 | echo "reinstalling all dependencies..." 116 | npm install 117 | 118 | echo "Run script to install the dev extension into the dev org in Azure DevOps..." 119 | node ./ops/deploy/dist/install-extension-to-dev-org.js "${INPUT_PARAM_AZ_EXT_NEW_VERSION}" 120 | if [[ ! $? -eq 0 ]]; then 121 | echo "failed installing dev extension at correct version" 122 | exit 1 123 | fi 124 | 125 | # Updating version in task.json file 126 | node "${PWD}/scripts/recovery-task-json-dev.js" 127 | 128 | echo "Extension installed" 129 | -------------------------------------------------------------------------------- /scripts/ci-deploy-preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2025 Snyk Ltd. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This script publish the extension to Marketplace. Preview Production deployment. 20 | # Arguments: 21 | # $1 - Extension version 22 | 23 | set -e 24 | 25 | # Install tfx 26 | echo "Installing tfx-cli globally..." 27 | sudo npm install -g tfx-cli@0.7.11 28 | 29 | # Build project 30 | "${PWD}/scripts/ci-build.sh" "prod" 31 | 32 | # Handle arguments 33 | AZ_EXT_NEW_VERSION="$1" 34 | 35 | # Updating version in task.json file 36 | node "${PWD}/scripts/ci-update-task-json-preview.js" ${AZ_EXT_NEW_VERSION} 37 | 38 | # Override version 39 | OVERRIDE_JSON="{ \"id\": \"${AZ_EXTENSION_ID}\", \"name\": \"${AZ_EXTENSION_NAME}\", \"version\": \"${AZ_EXT_NEW_VERSION}\", \"public\": true }" 40 | 41 | # Echo ENVs and publish extension 42 | echo "Publishing preview extension to Azure DevOps Marketplace..." 43 | echo "Version: ${AZ_EXT_NEW_VERSION}" 44 | echo "Extension ID: ${AZ_EXTENSION_ID}" 45 | echo "Publisher: ${AZ_PUBLISHER}" 46 | echo "Override JSON: ${OVERRIDE_JSON}" 47 | 48 | tfx extension publish --manifest-globs vss-extension-preview.json \ 49 | --version $AZ_EXT_NEW_VERSION \ 50 | --extension-id $AZ_EXTENSION_ID \ 51 | --publisher $AZ_PUBLISHER \ 52 | --override $OVERRIDE_JSON \ 53 | --token $AZURE_DEVOPS_EXT_PAT 54 | 55 | publish_exit_code=$? 56 | if [[ publish_exit_code -eq 0 ]]; then 57 | echo "Preview Extension published: ${AZ_EXT_NEW_VERSION}" 58 | else 59 | echo "Preview Extension failed to pubish with exit code ${publish_exit_code}" 60 | exit ${publish_exit_code} 61 | fi 62 | -------------------------------------------------------------------------------- /scripts/ci-deploy-prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 Snyk Ltd. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This script publish the extension to Marketplace. Production deployment. 20 | # Arguments: 21 | # $1 - Extension version 22 | 23 | # Handle arguments 24 | AZ_EXT_NEW_VERSION="$1" 25 | 26 | # Updating version in task.json file 27 | node "${PWD}/scripts/ci-update-task-json-prod.js" ${AZ_EXT_NEW_VERSION} 28 | 29 | # Override version 30 | OVERRIDE_JSON="{ \"id\": \"${AZ_EXTENSION_ID}\", \"name\": \"${AZ_EXTENSION_NAME}\", \"version\": \"${AZ_EXT_NEW_VERSION}\", \"public\": true }" 31 | 32 | # Publish extension 33 | echo "Publishing extension..." 34 | tfx extension publish --manifest-globs vss-extension.json \ 35 | --version $AZ_EXT_NEW_VERSION \ 36 | --extension-id $AZ_EXTENSION_ID \ 37 | --publisher $AZ_PUBLISHER \ 38 | --override $OVERRIDE_JSON \ 39 | --token $AZURE_DEVOPS_EXT_PAT 40 | 41 | publish_exit_code=$? 42 | if [[ publish_exit_code -eq 0 ]]; then 43 | echo "Extension published" 44 | else 45 | echo "Extension failed to pubish with exit code ${publish_exit_code}" 46 | exit ${publish_exit_code} 47 | fi 48 | -------------------------------------------------------------------------------- /scripts/ci-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 Snyk Ltd. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This script deploy the Snyk Azure Extension via CI tool when there is a merge 20 | # to the main branch. 21 | # The version is generated automatic via semantic versioning tool and the main branch 22 | # will be tag with the version. 23 | # $1 - Extension version 24 | # $2 (Optional) - Organization that will be shared the extension 25 | 26 | set -e 27 | 28 | echo "Deploying extension..." 29 | # Handle arguments 30 | pattern="[0-9]+\.[0-9]+\.[0-9]+" 31 | AZ_EXT_NEW_VERSION="$1" 32 | if [[ ! "${AZ_EXT_NEW_VERSION}" =~ $pattern ]]; then 33 | echo "Version is required." 34 | exit 1 35 | fi 36 | echo "Version: ${AZ_EXT_NEW_VERSION}" 37 | 38 | if [[ ! -z "$2" ]]; then 39 | AZ_ORG="$2" 40 | echo "Org: ${AZ_ORG}" 41 | fi 42 | PWD=$(pwd) 43 | echo "PWD: ${PWD}" 44 | 45 | set +e 46 | # check if tfx is installed and if not, install it 47 | tfx version >/dev/null 2>&1 48 | if [[ ! $? -eq 0 ]]; then 49 | echo "Check thinks tfx-cli is not installed" 50 | # echo "Installing tfx-cli globally..." 51 | # sudo npm install -g tfx-cli@0.7.11 52 | else 53 | echo "Check thinks tfx-cli already installed" 54 | fi 55 | set -e 56 | 57 | # install regardless of check until check working 58 | echo "Installing tfx-cli globally..." 59 | sudo npm install -g tfx-cli@0.7.11 60 | 61 | # Build project 62 | "${PWD}/scripts/ci-build.sh" "prod" 63 | 64 | if [[ ! -z "${AZ_ORG}" ]]; then 65 | "${PWD}/scripts/ci-deploy-dev.sh" ${AZ_EXT_NEW_VERSION} ${AZ_ORG} 66 | else 67 | "${PWD}/scripts/ci-deploy-prod.sh" ${AZ_EXT_NEW_VERSION} 68 | fi 69 | -------------------------------------------------------------------------------- /scripts/ci-update-task-json-preview.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | 19 | console.log('Replacing version snykTask/task.json file...'); 20 | // Get version from argument 21 | const version = process.argv[2]; 22 | 23 | const taskId = process.env.PREVIEW_AZ_TASK_ID; 24 | const taskName = process.env.AZ_TASK_NAME; 25 | const taskFriendlyName = process.env.AZ_TASK_FRIENDLY_NAME; 26 | 27 | if (!version.match(/^\d{4}\.\d{1,2}\.\d{1,2}\d{4}$/)) { 28 | console.log('Invalid version: ', version); 29 | process.exitCode = 1; 30 | process.exit(); 31 | } 32 | 33 | // Break version and create the JSON to be replaced 34 | const metaVersion = version.split('.'); 35 | const taskVersion = { 36 | Major: metaVersion[0], 37 | Minor: metaVersion[1], 38 | Patch: metaVersion[2], 39 | }; 40 | 41 | console.log('taskVersion: ', taskVersion); 42 | 43 | // Replace version in the snykTask/task.json file 44 | const filePath = './snykTask/task.json'; 45 | const taskJSON_File = JSON.parse(fs.readFileSync(filePath, 'utf8')); 46 | 47 | taskJSON_File['id'] = taskId; 48 | taskJSON_File['name'] = taskName; 49 | taskJSON_File['friendlyName'] = taskFriendlyName; 50 | taskJSON_File['version'] = taskVersion; 51 | 52 | fs.writeFileSync(filePath, JSON.stringify(taskJSON_File, null, 2), 'utf8'); 53 | 54 | console.log( 55 | 'Version replaced in snykTask/task.json file with: ', 56 | taskJSON_File['version'], 57 | ); 58 | -------------------------------------------------------------------------------- /scripts/ci-update-task-json-prod.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | 19 | console.log('Replacing version snykTask/task.json file...'); 20 | // Get version from argument 21 | const version = process.argv[2]; 22 | if (!version.match(/[0-9]+\.[0-9]+\.[0-9]+/)) { 23 | console.log('Invalid version: ', version); 24 | process.exitCode = 1; 25 | process.exit(); 26 | } 27 | 28 | // Break version and create the JSON to be replaced 29 | const metaVersion = version.split('.'); 30 | const taskVersion = { 31 | Major: metaVersion[0], 32 | Minor: metaVersion[1], 33 | Patch: metaVersion[2], 34 | }; 35 | console.log('taskVersion: ', taskVersion); 36 | 37 | // Replace version in the snykTask/task.json file 38 | const filePath = './snykTask/task.json'; 39 | const taskJSON_File = JSON.parse(fs.readFileSync(filePath, 'utf8')); 40 | taskJSON_File['version'] = taskVersion; 41 | fs.writeFileSync(filePath, JSON.stringify(taskJSON_File, null, 2), 'utf8'); 42 | console.log('Version replaced in snykTask/task.json file'); 43 | -------------------------------------------------------------------------------- /scripts/recovery-task-json-dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | 19 | const filePath = './snykTask/task.json'; 20 | const fileBakPath = './snykTask/task.json.bak'; 21 | 22 | console.log('Recovery snykTask/task.json file...'); 23 | const taskJSON_File = JSON.parse(fs.readFileSync(fileBakPath, 'utf8')); 24 | fs.writeFileSync(filePath, JSON.stringify(taskJSON_File, null, 2), 'utf8'); 25 | console.log('File recovered'); 26 | -------------------------------------------------------------------------------- /scripts/update-task-json-dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const { exit } = require('process'); 19 | 20 | console.log('Replacing version snykTask/task.json file...'); 21 | // Get version from argument 22 | const version = process.argv[2]; 23 | if (!version.match(/[0-9]+\.[0-9]+\.[0-9]+/)) { 24 | console.log('Invalid version: ', version); 25 | process.exitCode = 1; 26 | process.exit(); 27 | } 28 | 29 | const taskId = process.env.DEV_AZ_TASK_ID; // don't use the production GUID for dev/test deploys 30 | const taskName = process.env.DEV_AZ_TASK_NAME; 31 | const taskFriendlyName = process.env.DEV_AZ_TASK_FRIENDLY_NAME; 32 | 33 | if (!taskId) { 34 | console.log(`taskId not set! failing`); 35 | process.exit(1); 36 | } 37 | 38 | if (!taskName) { 39 | console.log(`taskName not set! failing`); 40 | process.exit(1); 41 | } 42 | 43 | if (!taskFriendlyName) { 44 | console.log(`taskFriendlyName not set! failing`); 45 | process.exit(1); 46 | } 47 | 48 | // Break version and create the JSON to be replaced 49 | const metaVersion = version.split('.'); 50 | const taskVersion = { 51 | Major: metaVersion[0], 52 | Minor: metaVersion[1], 53 | Patch: metaVersion[2], 54 | }; 55 | console.log('taskVersion: ', taskVersion); 56 | 57 | // Replace version in the snykTask/task.json file 58 | const filePath = './snykTask/task.json'; 59 | const fileBakPath = './snykTask/task.json.bak'; 60 | const taskJsonFileObj = JSON.parse(fs.readFileSync(filePath, 'utf8')); 61 | fs.writeFileSync(fileBakPath, JSON.stringify(taskJsonFileObj, null, 2), 'utf8'); 62 | 63 | // update information 64 | taskJsonFileObj['version'] = taskVersion; 65 | taskJsonFileObj['id'] = taskId; 66 | taskJsonFileObj['name'] = taskName; 67 | taskJsonFileObj['friendlyName'] = taskFriendlyName; 68 | fs.writeFileSync(filePath, JSON.stringify(taskJsonFileObj, null, 2), 'utf8'); 69 | 70 | console.log('Version replaced in snykTask/task.json file'); 71 | -------------------------------------------------------------------------------- /snykTask/.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.25.0 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-MOCKERY-3043117: 6 | - '*': 7 | reason: No upgrade available 8 | expires: 2024-07-18T00:00:00.000Z 9 | created: 2023-07-18T12:52:43.840Z 10 | patch: {} 11 | -------------------------------------------------------------------------------- /snykTask/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/snyk-azure-pipelines-task/f5212028be27d4ff47381cc299489fbe5d165e41/snykTask/icon.png -------------------------------------------------------------------------------- /snykTask/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "azure-pipelines-task-lib": "4.7.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /snykTask/src/__tests__/install/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { downloadExecutable, getSnykDownloadInfo } from '../../install'; 18 | import { Platform } from 'azure-pipelines-task-lib/task'; 19 | import * as nock from 'nock'; 20 | import * as os from 'os'; 21 | import * as path from 'path'; 22 | import * as uuid from 'uuid/v4'; 23 | 24 | describe('getSnykDownloadInfo', () => { 25 | it('retrieves the correct download info for Linux', () => { 26 | const dlInfo = getSnykDownloadInfo(Platform.Linux); 27 | expect(dlInfo).toEqual({ 28 | snyk: { 29 | filename: 'snyk-linux', 30 | downloadUrl: 31 | 'https://downloads.snyk.io/cli/stable/snyk-linux?utm_source=AZURE_PIPELINES', 32 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-linux', 33 | }, 34 | snykToHtml: { 35 | filename: 'snyk-to-html-linux', 36 | downloadUrl: 37 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-linux?utm_source=AZURE_PIPELINES', 38 | fallbackUrl: 39 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-linux', 40 | }, 41 | }); 42 | }); 43 | 44 | it('retrieves the correct download info for Windows', () => { 45 | const dlInfo = getSnykDownloadInfo(Platform.Windows); 46 | expect(dlInfo).toEqual({ 47 | snyk: { 48 | filename: 'snyk-win.exe', 49 | downloadUrl: 50 | 'https://downloads.snyk.io/cli/stable/snyk-win.exe?utm_source=AZURE_PIPELINES', 51 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-win.exe', 52 | }, 53 | snykToHtml: { 54 | filename: 'snyk-to-html-win.exe', 55 | downloadUrl: 56 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-win.exe?utm_source=AZURE_PIPELINES', 57 | fallbackUrl: 58 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-win.exe', 59 | }, 60 | }); 61 | }); 62 | 63 | it('retrieves the correct download info for MacOS', () => { 64 | const dlInfo = getSnykDownloadInfo(Platform.MacOS); 65 | expect(dlInfo).toEqual({ 66 | snyk: { 67 | filename: 'snyk-macos', 68 | downloadUrl: 69 | 'https://downloads.snyk.io/cli/stable/snyk-macos?utm_source=AZURE_PIPELINES', 70 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-macos', 71 | }, 72 | snykToHtml: { 73 | filename: 'snyk-to-html-macos', 74 | downloadUrl: 75 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-macos?utm_source=AZURE_PIPELINES', 76 | fallbackUrl: 77 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-macos', 78 | }, 79 | }); 80 | }); 81 | 82 | it('retrieves the correct download info a preview release', () => { 83 | const dlInfo = getSnykDownloadInfo(Platform.MacOS, 'preview '); 84 | expect(dlInfo).toEqual({ 85 | snyk: { 86 | filename: 'snyk-macos', 87 | downloadUrl: 88 | 'https://downloads.snyk.io/cli/preview/snyk-macos?utm_source=AZURE_PIPELINES', 89 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-macos', 90 | }, 91 | snykToHtml: { 92 | filename: 'snyk-to-html-macos', 93 | downloadUrl: 94 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-macos?utm_source=AZURE_PIPELINES', 95 | fallbackUrl: 96 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-macos', 97 | }, 98 | }); 99 | }); 100 | 101 | it('retrieves the correct download info for a valid semver', () => { 102 | const dlInfo = getSnykDownloadInfo(Platform.MacOS, '1.1287.0'); 103 | expect(dlInfo).toEqual({ 104 | snyk: { 105 | filename: 'snyk-macos', 106 | downloadUrl: 107 | 'https://downloads.snyk.io/cli/v1.1287.0/snyk-macos?utm_source=AZURE_PIPELINES', 108 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-macos', 109 | }, 110 | snykToHtml: { 111 | filename: 'snyk-to-html-macos', 112 | downloadUrl: 113 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-macos?utm_source=AZURE_PIPELINES', 114 | fallbackUrl: 115 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-macos', 116 | }, 117 | }); 118 | }); 119 | 120 | it('retrieves the correct download info for a valid semver and sanitizes input', () => { 121 | const dlInfo = getSnykDownloadInfo(Platform.MacOS, 'v1.1287.0 '); 122 | expect(dlInfo).toEqual({ 123 | snyk: { 124 | filename: 'snyk-macos', 125 | downloadUrl: 126 | 'https://downloads.snyk.io/cli/v1.1287.0/snyk-macos?utm_source=AZURE_PIPELINES', 127 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-macos', 128 | }, 129 | snykToHtml: { 130 | filename: 'snyk-to-html-macos', 131 | downloadUrl: 132 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-macos?utm_source=AZURE_PIPELINES', 133 | fallbackUrl: 134 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-macos', 135 | }, 136 | }); 137 | }); 138 | 139 | it('ignores invalid versions', () => { 140 | const dlInfo = getSnykDownloadInfo(Platform.MacOS, 'invalid-channel'); 141 | expect(dlInfo).toEqual({ 142 | snyk: { 143 | filename: 'snyk-macos', 144 | downloadUrl: 145 | 'https://downloads.snyk.io/cli/stable/snyk-macos?utm_source=AZURE_PIPELINES', 146 | fallbackUrl: 'https://static.snyk.io/cli/latest/snyk-macos', 147 | }, 148 | snykToHtml: { 149 | filename: 'snyk-to-html-macos', 150 | downloadUrl: 151 | 'https://downloads.snyk.io/snyk-to-html/latest/snyk-to-html-macos?utm_source=AZURE_PIPELINES', 152 | fallbackUrl: 153 | 'https://static.snyk.io/snyk-to-html/latest/snyk-to-html-macos', 154 | }, 155 | }); 156 | }); 157 | }); 158 | 159 | describe('downloadExecutable', () => { 160 | let mockConsoleError: jest.SpyInstance; 161 | 162 | beforeAll(() => { 163 | // Mock console.error to prevent logging during tests 164 | mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); 165 | }); 166 | 167 | beforeEach(() => { 168 | // Clear any existing mock server configuration 169 | nock.cleanAll(); 170 | mockConsoleError.mockClear(); 171 | }); 172 | 173 | afterAll(() => { 174 | // Clean up any remaining nock interceptors 175 | nock.cleanAll(); 176 | }); 177 | 178 | jest.setTimeout(30_000); 179 | it('gives up after all retries fail with 500 errors with meaningful error', async () => { 180 | // Mock the server to always respond with 500 errors 181 | const fileName = `test-file-${uuid()}.exe`; 182 | nock('https://example.com') 183 | .get('/' + fileName) 184 | .reply(500); 185 | 186 | const targetDirectory = path.join(os.tmpdir()); 187 | 188 | await downloadExecutable( 189 | targetDirectory, 190 | { 191 | filename: fileName, 192 | downloadUrl: 'https://example.com/' + fileName, 193 | fallbackUrl: '', 194 | }, 195 | 1, 196 | ); 197 | 198 | // Assert that the file was not created 199 | const calls = mockConsoleError.mock.calls; 200 | console.log(mockConsoleError.mock.calls); 201 | expect(mockConsoleError).toBeCalledTimes(4); 202 | expect(calls[0]).toEqual([`Download of ${fileName} failed: HTTP 500`]); 203 | expect(calls[1]).toEqual([ 204 | `All retries failed for ${fileName} from https://example.com/${fileName}: HTTP 500`, 205 | ]); 206 | }); 207 | 208 | it('gives up after all retries fail with 404 errors with meaningful error', async () => { 209 | // Mock the server to always respond with 404 errors 210 | const fileName = `test-file-${uuid()}.exe`; 211 | nock('https://example.com') 212 | .get('/' + fileName) 213 | .times(2) 214 | .reply(404); 215 | 216 | const targetDirectory = path.join(os.tmpdir()); 217 | 218 | await downloadExecutable( 219 | targetDirectory, 220 | { 221 | filename: fileName, 222 | downloadUrl: 'https://example.com/' + fileName, 223 | fallbackUrl: '' + fileName, 224 | }, 225 | 1, 226 | ); 227 | 228 | // Assert that the file was not created 229 | const calls = mockConsoleError.mock.calls; 230 | expect(mockConsoleError).toBeCalledTimes(4); 231 | expect(calls[0]).toEqual([`Download of ${fileName} failed: HTTP 404`]); 232 | expect(calls[1]).toEqual([ 233 | `All retries failed for ${fileName} from https://example.com/${fileName}: HTTP 404`, 234 | ]); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /snykTask/src/__tests__/task-version.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | test('ensure we can read the version from the task.json file', () => { 18 | const mockFn = jest.fn().mockReturnValue(`{ 19 | "id": "some-id", 20 | "name": "SnykSecurityScan", 21 | "friendlyName": "Snyk Security Scan", 22 | "description": "Azure Pipelines Task for Snyk", 23 | "helpMarkDown": "", 24 | "category": "Utility", 25 | "author": "Snyk", 26 | "version": { 27 | "Major": 1, 28 | "Minor": 2, 29 | "Patch": 3 30 | }, 31 | "instanceNameFormat": "Snyk scan for open source vulnerabilities" 32 | }`); 33 | 34 | jest.doMock('fs', () => { 35 | return { 36 | readFileSync: mockFn, 37 | }; 38 | }); 39 | 40 | const taskVersionModule = require('../task-version'); 41 | const v: string = taskVersionModule.getTaskVersion('./snykTask/task.json'); 42 | expect(v).toBe('1.2.3'); 43 | expect(mockFn).toHaveBeenCalledTimes(1); 44 | }); 45 | -------------------------------------------------------------------------------- /snykTask/src/__tests__/test-auth-token-retrieved.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { EndpointAuthorization } from 'azure-pipelines-task-lib'; 18 | 19 | beforeEach(() => { 20 | jest.resetModules(); 21 | }); 22 | 23 | test('test auth token pulled from service connection', () => { 24 | const mockEndpointAuthorization = { 25 | parameters: { 26 | apitoken: 'some-token', 27 | }, 28 | scheme: 'somescheme', 29 | } as EndpointAuthorization; 30 | 31 | jest.doMock('azure-pipelines-task-lib/task', () => { 32 | return { 33 | // getInput: jest.fn((name: string, required?: boolean) => "mockValue") 34 | getInput: jest.fn((name: string) => { 35 | if (name === 'serviceConnectionEndpoint') { 36 | return 'some-serviceConnectionEndpoint'; 37 | } else if (name === 'authToken') { 38 | return null; 39 | } 40 | }), 41 | getEndpointAuthorization: jest 42 | .fn() 43 | .mockReturnValue(mockEndpointAuthorization), 44 | }; 45 | }); 46 | 47 | const att = require('../task-args'); 48 | const retrievedAuthToken = att.getAuthToken(); 49 | expect(retrievedAuthToken).toBe('some-token'); 50 | }); 51 | 52 | test('test auth token pulled from serviceConnectionEndpoint if both authToken and serviceConnectionEndpoint are set', () => { 53 | const mockEndpointAuthorization = { 54 | parameters: { 55 | apitoken: 'some-token-from-service-connection', 56 | }, 57 | scheme: 'somescheme', 58 | } as EndpointAuthorization; 59 | 60 | // defined not inline here so I can call toHaveBeenCalledTimes on it 61 | const mockFnGetEndpointAuthorization = jest 62 | .fn() 63 | .mockReturnValue(mockEndpointAuthorization); 64 | 65 | jest.doMock('azure-pipelines-task-lib/task', () => { 66 | return { 67 | // getInput: jest.fn((name: string, required?: boolean) => "mockValue") 68 | getInput: jest.fn((name: string) => { 69 | if (name === 'serviceConnectionEndpoint') { 70 | return 'some-serviceConnectionEndpoint'; 71 | } else if (name === 'authToken') { 72 | return 'some-test-auth-token'; 73 | } 74 | }), 75 | // getEndpointAuthorization: jest.fn().mockReturnValue(mockEndpointAuthorization) 76 | getEndpointAuthorization: mockFnGetEndpointAuthorization, 77 | }; 78 | }); 79 | 80 | const att = require('../task-args'); 81 | const retrievedAuthToken = att.getAuthToken(); 82 | expect(retrievedAuthToken).toBe('some-token-from-service-connection'); 83 | expect(mockFnGetEndpointAuthorization).toHaveBeenCalledTimes(1); 84 | }); 85 | 86 | test('test auth token pulled from authToken if both authToken set and serviceConnectionEndpoint is not', () => { 87 | const mockEndpointAuthorization = { 88 | parameters: { 89 | apitoken: 'some-token-from-service-connection', 90 | }, 91 | scheme: 'somescheme', 92 | } as EndpointAuthorization; 93 | 94 | // defined not inline here so I can call toHaveBeenCalledTimes on it 95 | const mockFnGetEndpointAuthorization = jest 96 | .fn() 97 | .mockReturnValue(mockEndpointAuthorization); 98 | 99 | jest.doMock('azure-pipelines-task-lib/task', () => { 100 | return { 101 | // getInput: jest.fn((name: string, required?: boolean) => "mockValue") 102 | getInput: jest.fn((name: string) => { 103 | if (name === 'serviceConnectionEndpoint') { 104 | return null; 105 | } else if (name === 'authToken') { 106 | return 'some-test-auth-token'; 107 | } 108 | }), 109 | // getEndpointAuthorization: jest.fn().mockReturnValue(mockEndpointAuthorization) 110 | getEndpointAuthorization: mockFnGetEndpointAuthorization, 111 | }; 112 | }); 113 | 114 | const att = require('../task-args'); 115 | const retrievedAuthToken = att.getAuthToken(); 116 | expect(retrievedAuthToken).toBe('some-test-auth-token'); 117 | expect(mockFnGetEndpointAuthorization).toHaveBeenCalledTimes(0); 118 | }); 119 | 120 | test('test auth token returns empty string if both authToken set and serviceConnectionEndpoint are not set', () => { 121 | const mockEndpointAuthorization = { 122 | parameters: { 123 | apitoken: 'some-token-from-service-connection', 124 | }, 125 | scheme: 'somescheme', 126 | } as EndpointAuthorization; 127 | 128 | // defined not inline here so I can call toHaveBeenCalledTimes on it 129 | const mockFnGetEndpointAuthorization = jest 130 | .fn() 131 | .mockReturnValue(mockEndpointAuthorization); 132 | 133 | jest.doMock('azure-pipelines-task-lib/task', () => { 134 | return { 135 | // getInput: jest.fn((name: string, required?: boolean) => "mockValue") 136 | getInput: jest.fn((name: string) => { 137 | if (name === 'serviceConnectionEndpoint') { 138 | return null; 139 | } else if (name === 'authToken') { 140 | return null; 141 | } 142 | }), 143 | // getEndpointAuthorization: jest.fn().mockReturnValue(mockEndpointAuthorization) 144 | getEndpointAuthorization: mockFnGetEndpointAuthorization, 145 | }; 146 | }); 147 | 148 | const att = require('../task-args'); 149 | const retrievedAuthToken = att.getAuthToken(); 150 | expect(retrievedAuthToken).toBe(''); 151 | expect(mockFnGetEndpointAuthorization).toHaveBeenCalledTimes(0); 152 | }); 153 | -------------------------------------------------------------------------------- /snykTask/src/install/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Platform } from 'azure-pipelines-task-lib/task'; 18 | import * as fs from 'fs'; 19 | import * as path from 'path'; 20 | import * as https from 'https'; 21 | import { sanitizeVersionInput } from '../lib/sanitize-version-input'; 22 | 23 | export type Executable = { 24 | filename: string; 25 | downloadUrl: string; 26 | fallbackUrl: string; 27 | }; 28 | 29 | export type SnykDownloads = { 30 | snyk: Executable; 31 | snykToHtml: Executable; 32 | }; 33 | 34 | export function getSnykDownloadInfo( 35 | platform: Platform, 36 | versionString: string = 'stable', 37 | ): SnykDownloads { 38 | console.log( 39 | `Getting Snyk download info for platform: ${platform} version: ${versionString}`, 40 | ); 41 | 42 | const baseUrl = 'https://downloads.snyk.io'; 43 | const fallbackUrl = 'https://static.snyk.io'; 44 | const distributionChannel = sanitizeVersionInput(versionString); 45 | 46 | const filenameSuffixes: Record = { 47 | [Platform.Linux]: 'linux', 48 | [Platform.Windows]: 'win.exe', 49 | [Platform.MacOS]: 'macos', 50 | }; 51 | 52 | return { 53 | snyk: { 54 | filename: `snyk-${filenameSuffixes[platform]}`, 55 | downloadUrl: `${baseUrl}/cli/${distributionChannel}/snyk-${filenameSuffixes[platform]}?utm_source=AZURE_PIPELINES`, 56 | fallbackUrl: `${fallbackUrl}/cli/latest/snyk-${filenameSuffixes[platform]}`, 57 | }, 58 | snykToHtml: { 59 | filename: `snyk-to-html-${filenameSuffixes[platform]}`, 60 | downloadUrl: `${baseUrl}/snyk-to-html/latest/snyk-to-html-${filenameSuffixes[platform]}?utm_source=AZURE_PIPELINES`, 61 | fallbackUrl: `${fallbackUrl}/snyk-to-html/latest/snyk-to-html-${filenameSuffixes[platform]}`, 62 | }, 63 | }; 64 | } 65 | 66 | export async function downloadExecutable( 67 | targetDirectory: string, 68 | executable: Executable, 69 | maxRetries = 5, 70 | ) { 71 | const filePath = path.join(targetDirectory, executable.filename); 72 | console.log(`Downloading executable to: ${filePath}`); 73 | 74 | // Check if the file already exists 75 | if (fs.existsSync(filePath)) { 76 | console.log( 77 | `File ${executable.filename} already exists, skipping download.`, 78 | ); 79 | return; 80 | } 81 | 82 | const fileWriter = fs.createWriteStream(filePath, { 83 | mode: 0o766, 84 | }); 85 | 86 | // Wrapping the download in a function for easy retrying 87 | const doDownload = (urlString, filename) => 88 | new Promise((resolve, reject) => { 89 | const url = new URL(urlString); 90 | const requestOpts: https.RequestOptions = { 91 | host: url.hostname, 92 | path: url.pathname, 93 | timeout: 300000, // 5mins 94 | }; 95 | https 96 | .get(requestOpts, (response) => { 97 | const isResponseError = response.statusCode !== 200; 98 | 99 | response.on('finish', () => { 100 | console.log(`Response finished for ${urlString}`); 101 | }); 102 | response.on('close', () => { 103 | console.log(`Download connection closed for ${urlString}`); 104 | }); 105 | response.on('error', (err) => { 106 | console.error(`Download of ${filename} failed: ${err.message}`); 107 | reject(err); 108 | }); 109 | 110 | if (response.statusCode !== 200) { 111 | fileWriter.close(); 112 | } 113 | 114 | fileWriter.on('close', () => { 115 | console.log(`File.close ${filename} saved to ${filePath}`); 116 | if (isResponseError) { 117 | reject(new Error(`HTTP ${response.statusCode}`)); 118 | } else { 119 | resolve(); 120 | } 121 | }); 122 | 123 | response.pipe(fileWriter); 124 | }) 125 | .on('timeout', () => { 126 | console.error(`Download of ${filename} timed out`); 127 | reject(); 128 | }) 129 | .on('error', (err) => { 130 | console.error(`Request for ${filename} failed: ${err.message}`); 131 | reject(err); 132 | }); 133 | }); 134 | 135 | // Try to download the file, retry up to `maxRetries` times if the attempt fails 136 | for (let attempt = 0; attempt < maxRetries; attempt++) { 137 | try { 138 | console.log( 139 | `Downloading: ${executable.filename} from: ${executable.downloadUrl}`, 140 | ); 141 | await doDownload(executable.downloadUrl, executable.filename); 142 | console.log(`Download successful for ${executable.filename}`); 143 | return; 144 | } catch (err) { 145 | console.error( 146 | `Download of ${executable.filename} failed: ${err.message}`, 147 | ); 148 | 149 | // Don't wait before retrying the last attempt 150 | if (attempt < maxRetries - 1) { 151 | console.log( 152 | `Retrying download of ${executable.filename} from ${executable.downloadUrl} after 5 seconds...`, 153 | ); 154 | await new Promise((resolve) => setTimeout(resolve, 5000)); 155 | } else { 156 | console.error( 157 | `All retries failed for ${executable.filename} from ${executable.downloadUrl}: ${err.message}`, 158 | ); 159 | } 160 | } 161 | } 162 | 163 | // Try to download the file from fallback url, retry up to `maxRetries` times if the attempt fails 164 | for (let attempt = 0; attempt < maxRetries; attempt++) { 165 | try { 166 | console.log( 167 | `Downloading: ${executable.filename} from: ${executable.downloadUrl}`, 168 | ); 169 | await doDownload(executable.fallbackUrl, executable.filename); 170 | console.log(`Download successful for ${executable.filename}`); 171 | return; 172 | } catch (err) { 173 | console.error( 174 | `Download of ${executable.filename} failed: ${err.message}`, 175 | ); 176 | 177 | // Don't wait before retrying the last attempt 178 | if (attempt < maxRetries - 1) { 179 | console.log( 180 | `Retrying download of ${executable.filename} from ${executable.fallbackUrl} after 5 seconds...`, 181 | ); 182 | await new Promise((resolve) => setTimeout(resolve, 5000)); 183 | } else { 184 | console.error( 185 | `All retries failed for ${executable.filename} from ${executable.fallbackUrl}: ${err.message}`, 186 | ); 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /snykTask/src/lib/sanitize-version-input.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | export function sanitizeVersionInput(versionString: string = ''): string { 4 | const version = versionString.toLowerCase().trim(); 5 | const validDistributionChannels = ['stable', 'preview']; 6 | 7 | if (semver.valid(semver.clean(version))) { 8 | return `v${semver.clean(version)}`; 9 | } 10 | 11 | if (validDistributionChannels.includes(version)) { 12 | return version; 13 | } 14 | 15 | return 'stable'; 16 | } 17 | -------------------------------------------------------------------------------- /snykTask/src/task-args.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as tl from 'azure-pipelines-task-lib'; 18 | import { Severity, TestType, testTypeSeverityThreshold } from './task-lib'; 19 | 20 | export type MonitorWhen = 'never' | 'noIssuesFound' | 'always'; 21 | class TaskArgs { 22 | testType: string | undefined = 'app'; 23 | 24 | targetFile: string | undefined = ''; 25 | 26 | dockerImageName: string | undefined = ''; 27 | dockerfilePath: string | undefined = ''; 28 | 29 | severityThreshold: string | undefined = Severity.LOW; 30 | failOnThreshold: string = Severity.LOW; 31 | organization: string | undefined = ''; 32 | monitorWhen: MonitorWhen = 'always'; 33 | failOnIssues: boolean = true; 34 | projectName: string | undefined = ''; 35 | 36 | testDirectory: string | undefined = ''; 37 | additionalArguments: string = ''; 38 | ignoreUnknownCA: boolean = false; 39 | // Snyk Code severity with its own pickList (no critical) 40 | codeSeverityThreshold: string | undefined = Severity.LOW; 41 | 42 | delayAfterReportGenerationSeconds: number = 0; 43 | 44 | /** 45 | * The cli version to use 46 | * Defaults to 'stable', but can be set to 'preview' or a specific version such as '1.1287.0' 47 | */ 48 | distributionChannel: string = 'stable'; 49 | 50 | // the params here are the ones which are mandatory 51 | constructor(params: { failOnIssues: boolean }) { 52 | this.failOnIssues = params.failOnIssues; 53 | } 54 | 55 | public setMonitorWhen(rawInput?: string) { 56 | if (rawInput) { 57 | const lowerCaseInput = rawInput.toLowerCase(); 58 | if (this.testType == TestType.CODE) { 59 | console.log('Snyk Code publishes results using --report workflow'); 60 | this.monitorWhen = 'never'; 61 | } else if (lowerCaseInput === 'never' || lowerCaseInput === 'always') { 62 | this.monitorWhen = lowerCaseInput; 63 | } else if (lowerCaseInput === 'noissuesfound') { 64 | this.monitorWhen = 'noIssuesFound'; 65 | } else { 66 | console.log( 67 | `Invalid value for monitorWhen: '${rawInput}'. Ignoring this parameter.`, 68 | ); 69 | } 70 | } 71 | } 72 | 73 | // disallow snyk code monitor which follows --report workflow 74 | public shouldRunMonitor(snykTestSuccess: boolean): boolean { 75 | if (this.testType == TestType.CODE) { 76 | return false; 77 | } else if (this.monitorWhen === 'always') { 78 | return true; 79 | } else if (this.monitorWhen === 'never') { 80 | return false; 81 | } else { 82 | // noIssuesFound 83 | return snykTestSuccess; 84 | } 85 | } 86 | 87 | getFileParameter() { 88 | if (this.targetFile && !this.dockerImageName) { 89 | return this.targetFile; 90 | } 91 | 92 | if (this.dockerImageName && this.dockerfilePath) { 93 | return this.dockerfilePath; 94 | } 95 | 96 | if ( 97 | this.dockerImageName && 98 | !this.dockerfilePath && 99 | this.targetFile && 100 | this.targetFile.toLowerCase().includes('dockerfile') 101 | ) { 102 | return this.targetFile; 103 | } else { 104 | return ''; 105 | } 106 | } 107 | 108 | getProjectNameParameter() { 109 | if (!this.projectName) { 110 | return undefined; 111 | } 112 | 113 | if (this.projectName.indexOf(' ') >= 0) { 114 | console.log('project name contains space'); 115 | return `"${this.projectName}"`; 116 | } 117 | 118 | return this.projectName; 119 | } 120 | 121 | public getDistributionChannel(): string { 122 | return this.distributionChannel; 123 | } 124 | 125 | // validate based on testTypeSeverityThreshold applicable thresholds 126 | public validate() { 127 | const taskTestType = this.testType || TestType.APPLICATION; 128 | const validTestTypes: string[] = Object.values(TestType); 129 | const taskTestTypeThreshold = 130 | testTypeSeverityThreshold.get(taskTestType) ?? 131 | testTypeSeverityThreshold.get(TestType.APPLICATION); 132 | 133 | if (!validTestTypes.includes(taskTestType)) { 134 | const warningMsgTestType = `Invalid testType specified. Defaulted to 'app'`; 135 | console.log(warningMsgTestType); 136 | } 137 | 138 | if (this.failOnThreshold) { 139 | if ( 140 | !taskTestTypeThreshold?.includes(this.failOnThreshold.toLowerCase()) 141 | ) { 142 | const errorMsg = `If set, failOnThreshold must be one from [${taskTestTypeThreshold}] (case insensitive). If not set, the default is '${Severity.LOW}'.`; 143 | throw new Error(errorMsg); 144 | } 145 | } 146 | 147 | if ( 148 | taskTestType !== TestType.CODE && 149 | this.severityThreshold && 150 | !taskTestTypeThreshold?.includes(this.severityThreshold.toLowerCase()) 151 | ) { 152 | const errorMsg = `If set, severityThreshold must be one from [${taskTestTypeThreshold}] (case insensitive). If not set, the default is '${Severity.LOW}'.`; 153 | throw new Error(errorMsg); 154 | } 155 | 156 | if ( 157 | taskTestType === TestType.CODE && 158 | this.codeSeverityThreshold && 159 | !taskTestTypeThreshold?.includes(this.codeSeverityThreshold.toLowerCase()) 160 | ) { 161 | const errorMsg = `If set, codeSeverityThreshold must be one from [${taskTestTypeThreshold}] (case insensitive). If not set, the default is '${Severity.LOW}'.`; 162 | throw new Error(errorMsg); 163 | } 164 | } 165 | } 166 | 167 | export function getAuthToken() { 168 | const serviceConnectionEndpoint = tl.getInput( 169 | 'serviceConnectionEndpoint', 170 | false, 171 | ); 172 | 173 | const authToken = tl.getInput('authToken', false); 174 | 175 | if (authToken && !serviceConnectionEndpoint) { 176 | return authToken; 177 | } else { 178 | // pull token from the service connection and fail if it is not set 179 | if (serviceConnectionEndpoint) { 180 | const endpointAuthorization = tl.getEndpointAuthorization( 181 | serviceConnectionEndpoint, 182 | false, 183 | ); 184 | 185 | if (endpointAuthorization) { 186 | const authTokenFromServiceConnection = 187 | endpointAuthorization.parameters['apitoken']; 188 | return authTokenFromServiceConnection; 189 | } 190 | } 191 | } 192 | return ''; 193 | } 194 | 195 | export { TaskArgs }; 196 | -------------------------------------------------------------------------------- /snykTask/src/task-lib.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { TaskArgs } from './task-args'; 18 | import * as tr from 'azure-pipelines-task-lib/toolrunner'; 19 | import * as tl from 'azure-pipelines-task-lib/task'; 20 | import stream = require('stream'); 21 | import * as fs from 'fs'; 22 | import * as path from 'path'; 23 | 24 | export const JSON_ATTACHMENT_TYPE = 'JSON_ATTACHMENT_TYPE'; 25 | export const HTML_ATTACHMENT_TYPE = 'HTML_ATTACHMENT_TYPE'; 26 | 27 | export const getOptionsToExecuteCmd = (taskArgs: TaskArgs): tr.IExecOptions => { 28 | return { 29 | cwd: taskArgs.testDirectory, 30 | failOnStdErr: false, 31 | ignoreReturnCode: true, 32 | } as tr.IExecOptions; 33 | }; 34 | 35 | export const getOptionsToExecuteSnykCLICommand = ( 36 | taskArgs: TaskArgs, 37 | taskNameForAnalytics: string, 38 | taskVersion: string, 39 | snykToken: string, 40 | ): tr.IExecOptions => { 41 | const options = { 42 | cwd: taskArgs.testDirectory, 43 | failOnStdErr: false, 44 | ignoreReturnCode: true, 45 | env: { 46 | ...process.env, 47 | SNYK_INTEGRATION_NAME: taskNameForAnalytics, 48 | SNYK_INTEGRATION_VERSION: taskVersion, 49 | SNYK_TOKEN: snykToken, 50 | } as tr.IExecOptions['env'], 51 | } as tr.IExecOptions; 52 | return options; 53 | }; 54 | 55 | export const getOptionsForSnykToHtml = ( 56 | htmlOutputFileFullPath: string, 57 | taskArgs: TaskArgs, 58 | ): tr.IExecOptions => { 59 | const writableString: stream.Writable = fs.createWriteStream( 60 | htmlOutputFileFullPath, 61 | ); 62 | 63 | return { 64 | cwd: taskArgs.testDirectory, 65 | failOnStdErr: false, 66 | ignoreReturnCode: true, 67 | outStream: writableString, 68 | } as tr.IExecOptions; 69 | }; 70 | 71 | export function formatDate(d: Date): string { 72 | return d.toISOString().split('.')[0].replace(/:/g, '-'); 73 | } 74 | 75 | export function attachReport(filePath: string, attachmentType: string) { 76 | if (fs.existsSync(filePath)) { 77 | const filename = path.basename(filePath); 78 | console.log(`${filePath} exists... attaching file`); 79 | tl.addAttachment(attachmentType, filename, filePath); 80 | } else { 81 | console.log(`${filePath} does not exist... cannot attach`); 82 | } 83 | } 84 | 85 | export function removeRegexFromFile( 86 | fileFullPath: string, 87 | regex: RegExp, 88 | debug = false, 89 | ) { 90 | if (fs.existsSync(fileFullPath)) { 91 | try { 92 | const data = fs.readFileSync(fileFullPath, { 93 | encoding: 'utf8', 94 | flag: 'r', 95 | }); 96 | const result = data.replace(regex, ''); 97 | fs.writeFileSync(fileFullPath, result); 98 | } catch (err) { 99 | if (debug) { 100 | console.log(`Removing ${regex} from ${fileFullPath} failed.`); 101 | } 102 | } 103 | } 104 | } 105 | 106 | export enum Severity { 107 | CRITICAL = 'critical', 108 | HIGH = 'high', 109 | MEDIUM = 'medium', 110 | LOW = 'low', 111 | } 112 | 113 | export enum TestType { 114 | APPLICATION = 'app', 115 | CODE = 'code', 116 | CONTAINER_IMAGE = 'container', 117 | } 118 | 119 | export const testTypeSeverityThreshold = new Map>([ 120 | [ 121 | TestType.APPLICATION, 122 | [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW], 123 | ], 124 | [TestType.CODE, [Severity.HIGH, Severity.MEDIUM, Severity.LOW]], 125 | [ 126 | TestType.CONTAINER_IMAGE, 127 | [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW], 128 | ], 129 | ]); 130 | 131 | export function getSeverityOrdinal(severity: string): number { 132 | switch (severity) { 133 | case Severity.CRITICAL: 134 | return 3; 135 | case Severity.HIGH: 136 | return 2; 137 | case Severity.MEDIUM: 138 | return 1; 139 | case Severity.LOW: 140 | return 0; 141 | } 142 | throw new Error(`Cannot get severity ordinal for ${severity} severity`); 143 | } 144 | 145 | const codeSeverityMap = { 146 | error: Severity.HIGH, 147 | warning: Severity.MEDIUM, 148 | info: Severity.LOW, 149 | note: Severity.LOW, 150 | none: Severity.LOW, 151 | }; 152 | 153 | export function doVulnerabilitiesExistForFailureThreshold( 154 | filePath: string, 155 | threshold: string, 156 | ): boolean { 157 | if (!fs.existsSync(filePath)) { 158 | console.log( 159 | `${filePath} does not exist...cannot use it to search for vulnerabilities, defaulting to detected`, 160 | ); 161 | return true; 162 | } 163 | 164 | const file = fs.readFileSync(filePath, 'utf8'); 165 | const json = JSON.parse(file); 166 | const thresholdOrdinal = getSeverityOrdinal(threshold); 167 | 168 | if (isSnykCodeOutput(json)) { 169 | return hasMatchingCodeIssues(json['runs'][0]['results'], thresholdOrdinal); 170 | } 171 | 172 | if (Array.isArray(json)) { 173 | return json.some((project) => 174 | hasMatchingVulnerabilities(project, threshold), 175 | ); 176 | } 177 | 178 | return ( 179 | hasMatchingVulnerabilities(json, threshold) || 180 | hasMatchingApplicationVulnerabilities(json, threshold) 181 | ); 182 | } 183 | 184 | function hasMatchingApplicationVulnerabilities( 185 | project: any, 186 | threshold: string, 187 | ) { 188 | const applications = project['applications']; 189 | if (Array.isArray(applications) && applications.length > 0) { 190 | for (const proj of applications) { 191 | if (hasMatchingVulnerabilities(proj, threshold)) { 192 | return true; 193 | } 194 | } 195 | } 196 | 197 | return false; 198 | } 199 | 200 | function hasMatchingVulnerabilities(project: any, threshold: string) { 201 | for (const vulnerability of project['vulnerabilities']) { 202 | if ( 203 | getSeverityOrdinal(vulnerability['severity']) >= 204 | getSeverityOrdinal(threshold) 205 | ) { 206 | return true; 207 | } 208 | } 209 | 210 | return false; 211 | } 212 | 213 | // finds code issues levels mapping to severity of matching or higher than threshold 214 | function hasMatchingCodeIssues(results: any, thresholdOrdinal: number) { 215 | for (const issue of results) { 216 | if ( 217 | getSeverityOrdinal(codeSeverityMap[issue['level']]) >= thresholdOrdinal 218 | ) { 219 | return true; 220 | } 221 | } 222 | return false; 223 | } 224 | 225 | // tests whether json content is a Snyk code cli output json 226 | function isSnykCodeOutput(jsonContent: any) { 227 | return jsonContent['$schema']; 228 | } 229 | -------------------------------------------------------------------------------- /snykTask/src/task-version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from 'fs'; 18 | 19 | export function getTaskVersion(taskJsonPath: string): string { 20 | const taskJsonFile = fs.readFileSync(taskJsonPath, 'utf8'); 21 | const taskObj = JSON.parse(taskJsonFile); 22 | const versionObj = taskObj.version; 23 | 24 | const versionString = `${versionObj.Major}.${versionObj.Minor}.${versionObj.Patch}`; 25 | return versionString; 26 | } 27 | -------------------------------------------------------------------------------- /snykTask/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "826d5fe9-3983-4643-b918-487964d7cc87", 3 | "name": "SnykSecurityScan", 4 | "friendlyName": "Snyk Security Scan", 5 | "description": "Azure Pipelines Task for Snyk", 6 | "helpMarkDown": "", 7 | "category": "Utility", 8 | "author": "Snyk", 9 | "version": { 10 | "Major": 0, 11 | "Minor": 2, 12 | "Patch": 2 13 | }, 14 | "instanceNameFormat": "Snyk scan for open source vulnerabilities", 15 | "groups": [ 16 | { 17 | "name": "additionalSettings", 18 | "displayName": "Additional Settings", 19 | "isExpanded": true 20 | }, 21 | { 22 | "name": "advanced", 23 | "displayName": "Advanced", 24 | "isExpanded": false 25 | } 26 | ], 27 | "inputs": [ 28 | { 29 | "name": "serviceConnectionEndpoint", 30 | "type": "connectedService:SnykAuth", 31 | "label": "Snyk API token", 32 | "required": true, 33 | "defaultValue": "", 34 | "helpMarkDown": "Choose your Snyk service connection from your Azure DevOps project settings." 35 | }, 36 | { 37 | "name": "testType", 38 | "type": "pickList", 39 | "label": "What do you want to test?", 40 | "defaultValue": "app", 41 | "required": true, 42 | "helpMarkDown": "What do you want to test?", 43 | "options": { 44 | "app": "Application", 45 | "code": "Code", 46 | "container": "Container Image" 47 | }, 48 | "properties": { 49 | "EditableOptions": "False" 50 | } 51 | }, 52 | { 53 | "name": "dockerImageName", 54 | "label": "Container Image Name", 55 | "type": "string", 56 | "required": true, 57 | "defaultValue": "", 58 | "helpMarkDown": "The image name if scanning a container image", 59 | "visibleRule": "testType = container" 60 | }, 61 | { 62 | "name": "dockerfilePath", 63 | "label": "Path to Dockerfile", 64 | "type": "string", 65 | "required": false, 66 | "defaultValue": "", 67 | "helpMarkDown": "Relative path to Dockerfile (relative to repo root or working directory, if set)", 68 | "visibleRule": "testType = container" 69 | }, 70 | { 71 | "name": "targetFile", 72 | "label": "Custom path to manifest file to test", 73 | "type": "string", 74 | "required": false, 75 | "defaultValue": "", 76 | "helpMarkDown": "Relative path to manifest file to test (relative to repo root or working directory, if set)", 77 | "visibleRule": "testType = app" 78 | }, 79 | { 80 | "name": "severityThreshold", 81 | "label": "Testing severity threshold", 82 | "type": "pickList", 83 | "required": false, 84 | "defaultValue": "low", 85 | "options": { 86 | "low": "Low (default)", 87 | "medium": "Medium", 88 | "high": "High", 89 | "critical": "Critical" 90 | }, 91 | "helpMarkDown": "The testing severity threshold. Leave blank for no threshold.", 92 | "visibleRule": "testType = app || testType = container" 93 | }, 94 | { 95 | "name": "codeSeverityThreshold", 96 | "label": "Code Testing severity threshold", 97 | "type": "pickList", 98 | "required": false, 99 | "defaultValue": "low", 100 | "options": { 101 | "low": "Low (default)", 102 | "medium": "Medium", 103 | "high": "High" 104 | }, 105 | "helpMarkDown": "Snyk Code testing severity threshold. Leave blank for no threshold.", 106 | "visibleRule": "testType = code" 107 | }, 108 | { 109 | "name": "monitorWhen", 110 | "label": "When to run Snyk monitor", 111 | "type": "pickList", 112 | "options": { 113 | "always": "always", 114 | "noIssuesFound": "noIssuesFound", 115 | "never": "never" 116 | }, 117 | "required": true, 118 | "defaultValue": "always", 119 | "helpMarkDown": "When to run Snyk Monitor", 120 | "visibleRule": "testType = app || testType = container" 121 | }, 122 | { 123 | "name": "failOnIssues", 124 | "label": "Fail build if Snyk finds issues", 125 | "type": "boolean", 126 | "required": true, 127 | "defaultValue": "true", 128 | "helpMarkDown": "Fail build if Snyk finds issues" 129 | }, 130 | { 131 | "name": "projectName", 132 | "label": "Project name in Snyk", 133 | "type": "string", 134 | "required": false, 135 | "defaultValue": "", 136 | "helpMarkDown": "What you want to call (or already have called) this project in Snyk", 137 | "groupName": "additionalSettings" 138 | }, 139 | { 140 | "name": "organization", 141 | "label": "Organization name (or ID) in Snyk", 142 | "type": "string", 143 | "required": false, 144 | "defaultValue": "", 145 | "helpMarkDown": "Organization name (or ID) in Snyk", 146 | "groupName": "additionalSettings" 147 | }, 148 | { 149 | "name": "testDirectory", 150 | "label": "Test (Working) Directory", 151 | "type": "filePath", 152 | "required": false, 153 | "defaultValue": "$(Build.SourcesDirectory)", 154 | "helpMarkDown": "Test (Working) Directory", 155 | "groupName": "advanced" 156 | }, 157 | { 158 | "name": "additionalArguments", 159 | "label": "Additional command-line args for Snyk CLI (advanced)", 160 | "type": "string", 161 | "required": false, 162 | "defaultValue": "", 163 | "helpMarkDown": "Additional command-line args for Snyk CLI (advanced)", 164 | "groupName": "advanced" 165 | }, 166 | { 167 | "name": "distributionChannel", 168 | "label": "Distribution channel", 169 | "type": "string", 170 | "required": false, 171 | "defaultValue": "stable", 172 | "helpMarkDown": "Defaults to 'stable', but can be set to 'preview' or a specific version such as '1.1287.0'", 173 | "groupName": "advanced" 174 | }, 175 | { 176 | "name": "failOnThreshold", 177 | "label": "The severity threshold that will cause a build failure. Works only when combined with the 'failOnIssues' parameter.", 178 | "type": "pickList", 179 | "required": false, 180 | "defaultValue": "low", 181 | "options": { 182 | "low": "Low (default)", 183 | "medium": "Medium", 184 | "high": "High", 185 | "critical": "Critical" 186 | }, 187 | "helpMarkDown": "The severity threshold that will cause a build failure. Works only when combined with the 'failOnIssues' parameter. 'critical' value is only applicable for 'app' or 'container' test types." 188 | } 189 | ], 190 | "execution": { 191 | "Node10": { 192 | "target": "./dist/index.js" 193 | }, 194 | "Node20_1": { 195 | "target": "./dist/index.js" 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/code-test-error-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 3 | "version": "2.1.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "SnykCode", 9 | "semanticVersion": "1.0.0", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "javascript/HardcodedNonCryptoSecret/test", 16 | "ruleIndex": 0, 17 | "level": "error", 18 | "message": { 19 | "text": "Avoid hardcoding values that are meant to be secret. Found a hardcoded string used in here.", 20 | "markdown": "Avoid hardcoding values that are meant to be secret. Found {0} used in {1}.", 21 | "arguments": [ 22 | "[a hardcoded string](0)", 23 | "[here](1)" 24 | ] 25 | }, 26 | "locations": [ 27 | { 28 | "physicalLocation": { 29 | "artifactLocation": { 30 | "uri": "myOwnRepo/src/__tests__/test-auth-token-retrieved.ts", 31 | "uriBaseId": "%SRCROOT%" 32 | }, 33 | "region": { 34 | "startLine": 26, 35 | "endLine": 26, 36 | "startColumn": 7, 37 | "endColumn": 15 38 | } 39 | } 40 | } 41 | ], 42 | "fingerprints": { 43 | "0": "6ada7144e0ba5ee8ec92228733394aefe9df7cb937fb5264118052df2ab7e579", 44 | "1": "851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.12d7a8d2.5b0e9934.a51e97f2.851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.cf33be24.5b0e9934.a51e97f2" 45 | }, 46 | "codeFlows": [ 47 | { 48 | "threadFlows": [ 49 | { 50 | "locations": [ 51 | { 52 | "location": { 53 | "id": 0, 54 | "physicalLocation": { 55 | "artifactLocation": { 56 | "uri": "myOwnRepo/src/__tests__/abc.ts", 57 | "uriBaseId": "%SRCROOT%" 58 | }, 59 | "region": { 60 | "startLine": 26, 61 | "endLine": 26, 62 | "startColumn": 17, 63 | "endColumn": 29 64 | } 65 | } 66 | } 67 | }, 68 | { 69 | "location": { 70 | "id": 1, 71 | "physicalLocation": { 72 | "artifactLocation": { 73 | "uri": "myOwnRepo/src/__tests__/def.ts", 74 | "uriBaseId": "%SRCROOT%" 75 | }, 76 | "region": { 77 | "startLine": 26, 78 | "endLine": 26, 79 | "startColumn": 7, 80 | "endColumn": 15 81 | } 82 | } 83 | } 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ], 90 | "properties": { 91 | "priorityScore": 500, 92 | "priorityScoreFactors": [ 93 | { 94 | "label": true, 95 | "type": "multipleOccurrence" 96 | }, 97 | { 98 | "label": true, 99 | "type": "hotFileSource" 100 | }, 101 | { 102 | "label": true, 103 | "type": "fixExamples" 104 | } 105 | ], 106 | "isAutofixable": false 107 | } 108 | } 109 | ], 110 | "properties": { 111 | "coverage": [ 112 | { 113 | "isSupported": true, 114 | "lang": "TypeScript", 115 | "files": 27, 116 | "type": "SUPPORTED" 117 | }, 118 | { 119 | "isSupported": true, 120 | "lang": "HTML", 121 | "files": 1, 122 | "type": "SUPPORTED" 123 | }, 124 | { 125 | "isSupported": true, 126 | "lang": "JavaScript", 127 | "files": 6, 128 | "type": "SUPPORTED" 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/code-test-no-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 3 | "version": "2.1.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "SnykCode", 9 | "semanticVersion": "1.0.0", 10 | "version": "1.0.0", 11 | "rules": [] 12 | } 13 | }, 14 | "results": [], 15 | "properties": { 16 | "coverage": [ 17 | { 18 | "isSupported": true, 19 | "lang": "TypeScript", 20 | "files": 27, 21 | "type": "SUPPORTED" 22 | }, 23 | { 24 | "isSupported": true, 25 | "lang": "JavaScript", 26 | "files": 6, 27 | "type": "SUPPORTED" 28 | }, 29 | { 30 | "isSupported": true, 31 | "lang": "HTML", 32 | "files": 1, 33 | "type": "SUPPORTED" 34 | }, 35 | { 36 | "isSupported": false, 37 | "lang": "HTML", 38 | "files": 2, 39 | "type": "FAILED_PARSING" 40 | } 41 | ] 42 | } 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /snykTask/test/fixtures/code-test-none-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 3 | "version": "2.1.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "SnykCode", 9 | "semanticVersion": "1.0.0", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "javascript/HardcodedNonCryptoSecret/test", 16 | "ruleIndex": 0, 17 | "level": "none", 18 | "message": { 19 | "text": "Avoid hardcoding values that are meant to be secret. Found a hardcoded string used in here.", 20 | "markdown": "Avoid hardcoding values that are meant to be secret. Found {0} used in {1}.", 21 | "arguments": [ 22 | "[a hardcoded string](0)", 23 | "[here](1)" 24 | ] 25 | }, 26 | "locations": [ 27 | { 28 | "physicalLocation": { 29 | "artifactLocation": { 30 | "uri": "myOwnRepo/src/__tests__/test-auth-token-retrieved.ts", 31 | "uriBaseId": "%SRCROOT%" 32 | }, 33 | "region": { 34 | "startLine": 26, 35 | "endLine": 26, 36 | "startColumn": 7, 37 | "endColumn": 15 38 | } 39 | } 40 | } 41 | ], 42 | "fingerprints": { 43 | "0": "6ada7144e0ba5ee8ec92228733394aefe9df7cb937fb5264118052df2ab7e579", 44 | "1": "851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.12d7a8d2.5b0e9934.a51e97f2.851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.cf33be24.5b0e9934.a51e97f2" 45 | }, 46 | "codeFlows": [ 47 | { 48 | "threadFlows": [ 49 | { 50 | "locations": [ 51 | { 52 | "location": { 53 | "id": 0, 54 | "physicalLocation": { 55 | "artifactLocation": { 56 | "uri": "myOwnRepo/src/__tests__/abc.ts", 57 | "uriBaseId": "%SRCROOT%" 58 | }, 59 | "region": { 60 | "startLine": 26, 61 | "endLine": 26, 62 | "startColumn": 17, 63 | "endColumn": 29 64 | } 65 | } 66 | } 67 | }, 68 | { 69 | "location": { 70 | "id": 1, 71 | "physicalLocation": { 72 | "artifactLocation": { 73 | "uri": "myOwnRepo/src/__tests__/def.ts", 74 | "uriBaseId": "%SRCROOT%" 75 | }, 76 | "region": { 77 | "startLine": 26, 78 | "endLine": 26, 79 | "startColumn": 7, 80 | "endColumn": 15 81 | } 82 | } 83 | } 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ], 90 | "properties": { 91 | "priorityScore": 500, 92 | "priorityScoreFactors": [ 93 | { 94 | "label": true, 95 | "type": "multipleOccurrence" 96 | }, 97 | { 98 | "label": true, 99 | "type": "hotFileSource" 100 | }, 101 | { 102 | "label": true, 103 | "type": "fixExamples" 104 | } 105 | ], 106 | "isAutofixable": false 107 | } 108 | } 109 | ], 110 | "properties": { 111 | "coverage": [ 112 | { 113 | "isSupported": true, 114 | "lang": "TypeScript", 115 | "files": 27, 116 | "type": "SUPPORTED" 117 | }, 118 | { 119 | "isSupported": true, 120 | "lang": "HTML", 121 | "files": 1, 122 | "type": "SUPPORTED" 123 | }, 124 | { 125 | "isSupported": true, 126 | "lang": "JavaScript", 127 | "files": 6, 128 | "type": "SUPPORTED" 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/code-test-note-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 3 | "version": "2.1.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "SnykCode", 9 | "semanticVersion": "1.0.0", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "javascript/HardcodedNonCryptoSecret/test", 16 | "ruleIndex": 0, 17 | "level": "note", 18 | "message": { 19 | "text": "Avoid hardcoding values that are meant to be secret. Found a hardcoded string used in here.", 20 | "markdown": "Avoid hardcoding values that are meant to be secret. Found {0} used in {1}.", 21 | "arguments": [ 22 | "[a hardcoded string](0)", 23 | "[here](1)" 24 | ] 25 | }, 26 | "locations": [ 27 | { 28 | "physicalLocation": { 29 | "artifactLocation": { 30 | "uri": "myOwnRepo/src/__tests__/test-auth-token-retrieved.ts", 31 | "uriBaseId": "%SRCROOT%" 32 | }, 33 | "region": { 34 | "startLine": 26, 35 | "endLine": 26, 36 | "startColumn": 7, 37 | "endColumn": 15 38 | } 39 | } 40 | } 41 | ], 42 | "fingerprints": { 43 | "0": "6ada7144e0ba5ee8ec92228733394aefe9df7cb937fb5264118052df2ab7e579", 44 | "1": "851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.12d7a8d2.5b0e9934.a51e97f2.851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.cf33be24.5b0e9934.a51e97f2" 45 | }, 46 | "codeFlows": [ 47 | { 48 | "threadFlows": [ 49 | { 50 | "locations": [ 51 | { 52 | "location": { 53 | "id": 0, 54 | "physicalLocation": { 55 | "artifactLocation": { 56 | "uri": "myOwnRepo/src/__tests__/abc.ts", 57 | "uriBaseId": "%SRCROOT%" 58 | }, 59 | "region": { 60 | "startLine": 26, 61 | "endLine": 26, 62 | "startColumn": 17, 63 | "endColumn": 29 64 | } 65 | } 66 | } 67 | }, 68 | { 69 | "location": { 70 | "id": 1, 71 | "physicalLocation": { 72 | "artifactLocation": { 73 | "uri": "myOwnRepo/src/__tests__/def.ts", 74 | "uriBaseId": "%SRCROOT%" 75 | }, 76 | "region": { 77 | "startLine": 26, 78 | "endLine": 26, 79 | "startColumn": 7, 80 | "endColumn": 15 81 | } 82 | } 83 | } 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ], 90 | "properties": { 91 | "priorityScore": 500, 92 | "priorityScoreFactors": [ 93 | { 94 | "label": true, 95 | "type": "multipleOccurrence" 96 | }, 97 | { 98 | "label": true, 99 | "type": "hotFileSource" 100 | }, 101 | { 102 | "label": true, 103 | "type": "fixExamples" 104 | } 105 | ], 106 | "isAutofixable": false 107 | } 108 | } 109 | ], 110 | "properties": { 111 | "coverage": [ 112 | { 113 | "isSupported": true, 114 | "lang": "TypeScript", 115 | "files": 27, 116 | "type": "SUPPORTED" 117 | }, 118 | { 119 | "isSupported": true, 120 | "lang": "HTML", 121 | "files": 1, 122 | "type": "SUPPORTED" 123 | }, 124 | { 125 | "isSupported": true, 126 | "lang": "JavaScript", 127 | "files": 6, 128 | "type": "SUPPORTED" 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/code-test-warning-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 3 | "version": "2.1.0", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "SnykCode", 9 | "semanticVersion": "1.0.0", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | "results": [ 14 | { 15 | "ruleId": "javascript/HardcodedNonCryptoSecret/test", 16 | "ruleIndex": 0, 17 | "level": "warning", 18 | "message": { 19 | "text": "Avoid hardcoding values that are meant to be secret. Found a hardcoded string used in here.", 20 | "markdown": "Avoid hardcoding values that are meant to be secret. Found {0} used in {1}.", 21 | "arguments": [ 22 | "[a hardcoded string](0)", 23 | "[here](1)" 24 | ] 25 | }, 26 | "locations": [ 27 | { 28 | "physicalLocation": { 29 | "artifactLocation": { 30 | "uri": "myOwnRepo/src/__tests__/test-auth-token-retrieved.ts", 31 | "uriBaseId": "%SRCROOT%" 32 | }, 33 | "region": { 34 | "startLine": 26, 35 | "endLine": 26, 36 | "startColumn": 7, 37 | "endColumn": 15 38 | } 39 | } 40 | } 41 | ], 42 | "fingerprints": { 43 | "0": "6ada7144e0ba5ee8ec92228733394aefe9df7cb937fb5264118052df2ab7e579", 44 | "1": "851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.12d7a8d2.5b0e9934.a51e97f2.851af641.bc7307ec.7acba8d2.c3c70106.79a7d027.cf33be24.5b0e9934.a51e97f2" 45 | }, 46 | "codeFlows": [ 47 | { 48 | "threadFlows": [ 49 | { 50 | "locations": [ 51 | { 52 | "location": { 53 | "id": 0, 54 | "physicalLocation": { 55 | "artifactLocation": { 56 | "uri": "myOwnRepo/src/__tests__/abc.ts", 57 | "uriBaseId": "%SRCROOT%" 58 | }, 59 | "region": { 60 | "startLine": 26, 61 | "endLine": 26, 62 | "startColumn": 17, 63 | "endColumn": 29 64 | } 65 | } 66 | } 67 | }, 68 | { 69 | "location": { 70 | "id": 1, 71 | "physicalLocation": { 72 | "artifactLocation": { 73 | "uri": "myOwnRepo/src/__tests__/def.ts", 74 | "uriBaseId": "%SRCROOT%" 75 | }, 76 | "region": { 77 | "startLine": 26, 78 | "endLine": 26, 79 | "startColumn": 7, 80 | "endColumn": 15 81 | } 82 | } 83 | } 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ], 90 | "properties": { 91 | "priorityScore": 500, 92 | "priorityScoreFactors": [ 93 | { 94 | "label": true, 95 | "type": "multipleOccurrence" 96 | }, 97 | { 98 | "label": true, 99 | "type": "hotFileSource" 100 | }, 101 | { 102 | "label": true, 103 | "type": "fixExamples" 104 | } 105 | ], 106 | "isAutofixable": false 107 | } 108 | } 109 | ], 110 | "properties": { 111 | "coverage": [ 112 | { 113 | "isSupported": true, 114 | "lang": "TypeScript", 115 | "files": 27, 116 | "type": "SUPPORTED" 117 | }, 118 | { 119 | "isSupported": true, 120 | "lang": "HTML", 121 | "files": 1, 122 | "type": "SUPPORTED" 123 | }, 124 | { 125 | "isSupported": true, 126 | "lang": "JavaScript", 127 | "files": 6, 128 | "type": "SUPPORTED" 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/container-app-vulnerabilities-critical.json: -------------------------------------------------------------------------------- 1 | { 2 | "vulnerabilities": [ 3 | { 4 | "severity": "medium" 5 | }, 6 | { 7 | "severity": "low" 8 | } 9 | ], 10 | "ok": true, 11 | "dependencyCount": 0, 12 | "org": "demo-applications", 13 | "applications": [ 14 | { 15 | "vulnerabilities": [ 16 | { 17 | "severity": "critical" 18 | }, 19 | { 20 | "severity": "high" 21 | } 22 | ], 23 | "ok": true, 24 | "dependencyCount": 0, 25 | "org": "demo-applications" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/container-app-vulnerabilities-medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "vulnerabilities": [ 3 | { 4 | "severity": "low" 5 | } 6 | ], 7 | "ok": true, 8 | "dependencyCount": 0, 9 | "org": "demo-applications", 10 | "applications": [ 11 | { 12 | "vulnerabilities": [ 13 | { 14 | "severity": "medium" 15 | }, 16 | { 17 | "severity": "medium" 18 | } 19 | ], 20 | "ok": true, 21 | "dependencyCount": 0, 22 | "org": "demo-applications" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/golang-no-code-issues/go.mod: -------------------------------------------------------------------------------- 1 | module app 2 | 3 | go 1.12 4 | 5 | require github.com/lib/pq v1.1.1 6 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/golang-no-code-issues/go.sum: -------------------------------------------------------------------------------- 1 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 2 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/golang-no-code-issues/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | // nothing to see here 5 | } 6 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/high-vulnerabilities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "vulnerabilities": [ 4 | { 5 | "severity": "critical" 6 | }, 7 | { 8 | "severity": "high" 9 | } 10 | ], 11 | "ok": true, 12 | "dependencyCount": 0, 13 | "org": "demo-applications" 14 | }, 15 | { 16 | "vulnerabilities": [ 17 | { 18 | "severity": "medium" 19 | }, 20 | { 21 | "severity": "high" 22 | } 23 | ], 24 | "ok": true, 25 | "dependencyCount": 0, 26 | "org": "demo-applications" 27 | } 28 | ] -------------------------------------------------------------------------------- /snykTask/test/fixtures/low-vulnerabilities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "vulnerabilities": [ 4 | { 5 | "severity": "medium" 6 | }, 7 | { 8 | "severity": "low" 9 | } 10 | ], 11 | "ok": true, 12 | "dependencyCount": 0, 13 | "org": "demo-applications" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/single-project-high-vulnerabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "vulnerabilities": [ 3 | { 4 | "severity": "critical" 5 | }, 6 | { 7 | "severity": "high" 8 | } 9 | ], 10 | "ok": true, 11 | "dependencyCount": 0, 12 | "org": "demo-applications" 13 | } 14 | 15 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/somejson.json: -------------------------------------------------------------------------------- 1 | { 2 | [command]/usr/local/bin/snyk-to-html -i /home/vsts/work/_temp/report-2020-11-29TLOLZ12-55-07.json 3 | "vulnerabilities": [], 4 | "ok": true, 5 | "dependencyCount": 0, 6 | "org": "demo-applications", 7 | "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.19.0\nignore: {}\npatch: {}\n", 8 | "isPrivate": true, 9 | "licensesPolicy": { 10 | "severities": {}, 11 | "orgLicenseRules": { 12 | "AGPL-1.0": { 13 | "licenseType": "AGPL-1.0", 14 | "severity": "high", 15 | "instructions": "" 16 | }, 17 | "AGPL-3.0": { 18 | "licenseType": "AGPL-3.0", 19 | "severity": "high", 20 | "instructions": "" 21 | }, 22 | 23 | "Artistic-1.0": { 24 | "licenseType": "Artistic-1.0", 25 | "severity": "medium", 26 | "instructions": "" 27 | }, 28 | "Artistic-2.0": { 29 | "licenseType": "Artistic-2.0", 30 | "severity": "medium", 31 | "instructions": "" 32 | }, 33 | "CDDL-1.0": { 34 | "licenseType": "CDDL-1.0", 35 | "severity": "medium", 36 | "instructions": "" 37 | }, 38 | "CPOL-1.02": { 39 | "licenseType": "CPOL-1.02", 40 | "severity": "high", 41 | "instructions": "" 42 | }, 43 | "EPL-1.0": { 44 | "licenseType": "EPL-1.0", 45 | "severity": "medium", 46 | "instructions": "" 47 | }, 48 | 49 | "GPL-2.0": { 50 | "licenseType": "GPL-2.0", 51 | "severity": "high", 52 | "instructions": "" 53 | }, 54 | "GPL-3.0": { 55 | "licenseType": "GPL-3.0", 56 | "severity": "high", 57 | "instructions": "" 58 | }, 59 | "LGPL-2.0": { 60 | "licenseType": "LGPL-2.0", 61 | "severity": "medium", 62 | "instructions": "" 63 | }, 64 | "LGPL-2.1": { 65 | "licenseType": "LGPL-2.1", 66 | "severity": "medium", 67 | "instructions": "" 68 | }, 69 | "LGPL-3.0": { 70 | "licenseType": "LGPL-3.0", 71 | "severity": "medium", 72 | "instructions": "" 73 | }, 74 | "MPL-1.1": { 75 | "licenseType": "MPL-1.1", 76 | "severity": "medium", 77 | "instructions": "" 78 | }, 79 | [command]/usr/local/bin/snyk-to-html -i /home/vsts/work/_temp/report-2020-11-29TLOLZ12-55-07.json 80 | "MPL-2.0": { 81 | "licenseType": "MPL-2.0", 82 | "severity": "medium", 83 | "instructions": "" 84 | }, 85 | "MS-RL": { 86 | "licenseType": "MS-RL", 87 | "severity": "medium", 88 | "instructions": "" 89 | }, 90 | "SimPL-2.0": { 91 | "licenseType": "SimPL-2.0", 92 | "severity": "high", 93 | "instructions": "" 94 | } 95 | } 96 | }, 97 | "packageManager": "npm", 98 | "projectId": "a22aafd9-26a6-4609-957d-52c3c65bdf2c", 99 | "ignoreSettings": null, 100 | "summary": "No known vulnerabilities", 101 | "filesystemPolicy": false, 102 | "uniqueCount": 0, 103 | "projectName": "empty-package-json", 104 | "foundProjectCount": 1, 105 | "displayTargetFile": "another-project/package-lock.json", 106 | "path": "/home/vsts/work/1/s" 107 | } 108 | -------------------------------------------------------------------------------- /snykTask/test/fixtures/somejsonAfterNonglobal.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "vulnerabilities": [], 4 | "ok": true, 5 | "dependencyCount": 0, 6 | "org": "demo-applications", 7 | "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.19.0\nignore: {}\npatch: {}\n", 8 | "isPrivate": true, 9 | "licensesPolicy": { 10 | "severities": {}, 11 | "orgLicenseRules": { 12 | "AGPL-1.0": { 13 | "licenseType": "AGPL-1.0", 14 | "severity": "high", 15 | "instructions": "" 16 | }, 17 | "AGPL-3.0": { 18 | "licenseType": "AGPL-3.0", 19 | "severity": "high", 20 | "instructions": "" 21 | }, 22 | 23 | "Artistic-1.0": { 24 | "licenseType": "Artistic-1.0", 25 | "severity": "medium", 26 | "instructions": "" 27 | }, 28 | "Artistic-2.0": { 29 | "licenseType": "Artistic-2.0", 30 | "severity": "medium", 31 | "instructions": "" 32 | }, 33 | "CDDL-1.0": { 34 | "licenseType": "CDDL-1.0", 35 | "severity": "medium", 36 | "instructions": "" 37 | }, 38 | "CPOL-1.02": { 39 | "licenseType": "CPOL-1.02", 40 | "severity": "high", 41 | "instructions": "" 42 | }, 43 | "EPL-1.0": { 44 | "licenseType": "EPL-1.0", 45 | "severity": "medium", 46 | "instructions": "" 47 | }, 48 | 49 | "GPL-2.0": { 50 | "licenseType": "GPL-2.0", 51 | "severity": "high", 52 | "instructions": "" 53 | }, 54 | "GPL-3.0": { 55 | "licenseType": "GPL-3.0", 56 | "severity": "high", 57 | "instructions": "" 58 | }, 59 | "LGPL-2.0": { 60 | "licenseType": "LGPL-2.0", 61 | "severity": "medium", 62 | "instructions": "" 63 | }, 64 | "LGPL-2.1": { 65 | "licenseType": "LGPL-2.1", 66 | "severity": "medium", 67 | "instructions": "" 68 | }, 69 | "LGPL-3.0": { 70 | "licenseType": "LGPL-3.0", 71 | "severity": "medium", 72 | "instructions": "" 73 | }, 74 | "MPL-1.1": { 75 | "licenseType": "MPL-1.1", 76 | "severity": "medium", 77 | "instructions": "" 78 | }, 79 | [command]/usr/local/bin/snyk-to-html -i /home/vsts/work/_temp/report-2020-11-29TLOLZ12-55-07.json 80 | "MPL-2.0": { 81 | "licenseType": "MPL-2.0", 82 | "severity": "medium", 83 | "instructions": "" 84 | }, 85 | "MS-RL": { 86 | "licenseType": "MS-RL", 87 | "severity": "medium", 88 | "instructions": "" 89 | }, 90 | "SimPL-2.0": { 91 | "licenseType": "SimPL-2.0", 92 | "severity": "high", 93 | "instructions": "" 94 | } 95 | } 96 | }, 97 | "packageManager": "npm", 98 | "projectId": "a22aafd9-26a6-4609-957d-52c3c65bdf2c", 99 | "ignoreSettings": null, 100 | "summary": "No known vulnerabilities", 101 | "filesystemPolicy": false, 102 | "uniqueCount": 0, 103 | "projectName": "empty-package-json", 104 | "foundProjectCount": 1, 105 | "displayTargetFile": "another-project/package-lock.json", 106 | "path": "/home/vsts/work/1/s" 107 | } 108 | -------------------------------------------------------------------------------- /snykTask/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // revert to es6 /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "./dist" /* Redirect output structure to the directory. */, 6 | 7 | /* Strict Type-Checking Options */ 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 10 | "skipLibCheck": true, 11 | 12 | /* Module Resolution Options */ 13 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 14 | 15 | /* Source Map Options */ 16 | "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */, 17 | "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */, 18 | "useUnknownInCatchVariables": false 19 | }, 20 | "exclude": ["**/__tests__/**"] 21 | } 22 | -------------------------------------------------------------------------------- /ui/enhancer/__tests__/snyk-report.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | Tread carefully 19 | =============== 20 | 21 | snyk-report.ts is importing (global) Azure dependencies that don't exists in this project 22 | thus TS compilation may fail on them, making it impossible for Jest to mock them 23 | */ 24 | 25 | import { generateReportTitle } from '../generate-report-title'; 26 | 27 | describe('SnykReportTab UI', () => { 28 | describe('generateReportTitle', () => { 29 | test('handling of single project without vulns', () => { 30 | const jsonResults = { 31 | packageManager: 'nuget', 32 | uniqueCount: 0, 33 | }; 34 | const title = generateReportTitle( 35 | jsonResults, 36 | 'report-2021-04-27T13-44-14.json', 37 | ); 38 | expect(title).toEqual( 39 | 'Snyk Test for nuget (report-2021-04-27 13:44:14) | No issues found', 40 | ); 41 | }); 42 | 43 | test('handling of single project with vulns', () => { 44 | const jsonResults = { 45 | packageManager: 'npm', 46 | uniqueCount: 3, 47 | }; 48 | const title = generateReportTitle( 49 | jsonResults, 50 | 'report-2021-04-27T13-44-14.json', 51 | ); 52 | expect(title).toEqual( 53 | 'Snyk Test for npm (report-2021-04-27 13:44:14) | Found 3 issues', 54 | ); 55 | }); 56 | 57 | test('handling of multiple projects with vulns', () => { 58 | const jsonResults = [ 59 | { 60 | packageManager: 'npm', 61 | uniqueCount: 3, 62 | }, 63 | { 64 | packageManager: 'npm', 65 | uniqueCount: 2, 66 | }, 67 | ]; 68 | const title = generateReportTitle( 69 | jsonResults, 70 | 'report-2021-04-27T13-44-14.json', 71 | ); 72 | expect(title).toEqual('Tested 2 npm projects | Found 5 issues'); 73 | }); 74 | 75 | test('handling of multiple projects with vulns', () => { 76 | const jsonResults = [ 77 | { 78 | packageManager: 'ruby', 79 | uniqueCount: 3, 80 | }, 81 | { 82 | packageManager: 'ruby', 83 | uniqueCount: 1, 84 | }, 85 | { 86 | packageManager: 'yarn', 87 | uniqueCount: 0, 88 | }, 89 | { 90 | packageManager: 'python', 91 | uniqueCount: 0, 92 | }, 93 | ]; 94 | const title = generateReportTitle( 95 | jsonResults, 96 | 'report-2021-04-27T13-44-14.json', 97 | ); 98 | expect(title).toEqual( 99 | 'Tested 4 ruby/yarn/python projects | Found 4 issues', 100 | ); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /ui/enhancer/detect-vulns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export function detectVulns(jsonResults: object | any[]): boolean { 18 | if (Array.isArray(jsonResults)) { 19 | return jsonResults.some((result) => !!result.uniqueCount); 20 | } 21 | 22 | if ( 23 | (jsonResults['uniqueCount'] && jsonResults['uniqueCount'] > 0) || 24 | (jsonResults['$schema'] && jsonResults['runs'][0]['results'].length > 0) 25 | ) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | -------------------------------------------------------------------------------- /ui/enhancer/generate-report-title.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { detectVulns } from './detect-vulns'; 18 | 19 | export function generateReportTitle( 20 | jsonResults: object | any[], 21 | attachmentName: string, // timestamp e.g., 'report-2021-04-27T13-44-14.json' 22 | ): string { 23 | const vulnsFound = detectVulns(jsonResults); 24 | 25 | // --all-projects flag may return results in an array 26 | if (Array.isArray(jsonResults)) { 27 | const vulnsCount: number = jsonResults.reduce( 28 | (issuesFound, result): number => 29 | result.uniqueCount ? issuesFound + result.uniqueCount : issuesFound, 30 | 0, 31 | ); 32 | const uniquePackageManagers = Array.from( 33 | new Set( 34 | jsonResults.map((result) => result.packageManager).filter(Boolean), 35 | ), 36 | ).join('/'); 37 | 38 | let titleText = `Tested ${jsonResults.length} ${uniquePackageManagers} projects`; 39 | if (vulnsFound) { 40 | titleText += ` | Found ${vulnsCount} issue${vulnsCount === 1 ? '' : 's'}`; 41 | } else { 42 | titleText += ` | No issues found`; 43 | } 44 | 45 | return titleText; 46 | } 47 | 48 | // Single project scan or Snyk code scan results 49 | let titleText = ''; 50 | if (jsonResults['docker'] && jsonResults['docker']['baseImage']) { 51 | titleText = `Snyk Test for ${ 52 | jsonResults['docker']['baseImage'] 53 | } (${formatReportName(attachmentName)})`; 54 | } 55 | 56 | if (jsonResults['packageManager']) { 57 | titleText = `Snyk Test for ${ 58 | jsonResults['packageManager'] 59 | } (${formatReportName(attachmentName)})`; 60 | } 61 | 62 | if (jsonResults['$schema']) { 63 | titleText = `Snyk Code Test for (${formatReportName(attachmentName)})`; 64 | } 65 | 66 | if (jsonResults['uniqueCount'] && jsonResults['uniqueCount'] > 0) { 67 | titleText += ` | Found ${jsonResults['uniqueCount']} issues`; 68 | } else if ( 69 | jsonResults['$schema'] && 70 | jsonResults['runs'][0]['results'].length > 0 71 | ) { 72 | titleText += ` | Found ${jsonResults['runs'][0]['results'].length} issues`; 73 | } else { 74 | titleText += ` | No issues found`; 75 | } 76 | 77 | return titleText; 78 | } 79 | 80 | function formatReportName( 81 | name: string /* timestamp e.g., 'report-2021-04-27T13-44-14.json' */, 82 | ): string { 83 | const reportName = name.split('.')[0]; 84 | const tmpName = reportName.split('T'); 85 | return `${tmpName[0]} ${tmpName[1].replace(/-/g, ':')}`; 86 | } 87 | -------------------------------------------------------------------------------- /ui/enhancer/snyk-report.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Snyk Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as Controls from 'VSS/Controls'; 18 | import * as TFSBuildContracts from 'TFS/Build/Contracts'; 19 | import * as TFSBuildExtensionContracts from 'TFS/Build/ExtensionContracts'; 20 | import * as DTClient from 'TFS/DistributedTask/TaskRestClient'; 21 | import { generateReportTitle } from './generate-report-title'; 22 | import { detectVulns } from './detect-vulns'; 23 | 24 | const BUILD_PHASE = 'build'; 25 | const HTML_ATTACHMENT_TYPE = 'HTML_ATTACHMENT_TYPE'; 26 | const JSON_ATTACHMENT_TYPE = 'JSON_ATTACHMENT_TYPE'; 27 | 28 | export class SnykReportTab extends Controls.BaseControl { 29 | private projectId: string = ''; 30 | private planId: string = ''; 31 | private taskClient; 32 | private reportList: HTMLElement[] = []; 33 | 34 | constructor() { 35 | super(); 36 | } 37 | 38 | public initialize = (): void => { 39 | super.initialize(); 40 | // Get configuration that's shared between extension and the extension host 41 | const sharedConfig: TFSBuildExtensionContracts.IBuildResultsViewExtensionConfig = 42 | VSS.getConfiguration(); 43 | const vsoContext = VSS.getWebContext(); 44 | if (sharedConfig) { 45 | // register your extension with host through callback 46 | sharedConfig.onBuildChanged((build: TFSBuildContracts.Build) => { 47 | this.taskClient = DTClient.getClient(); 48 | this.projectId = vsoContext.project.id; 49 | this.planId = build.orchestrationPlan.planId; 50 | 51 | const reportListElem: HTMLElement = document.getElementById( 52 | 'reportList', 53 | ) as HTMLElement; 54 | 55 | this.taskClient 56 | .getPlanAttachments( 57 | this.projectId, 58 | BUILD_PHASE, 59 | this.planId, 60 | HTML_ATTACHMENT_TYPE, 61 | ) 62 | .then((taskAttachments) => { 63 | $.each(taskAttachments, (index, taskAttachment) => { 64 | const attachmentName = taskAttachment.name; 65 | const timelineId = taskAttachment.timelineId; 66 | const recordId = taskAttachment.recordId; 67 | 68 | if ( 69 | taskAttachment._links && 70 | taskAttachment._links.self && 71 | taskAttachment._links.self.href 72 | ) { 73 | const reportItem = this.createReportItem( 74 | index, 75 | attachmentName, 76 | timelineId, 77 | recordId, 78 | ); 79 | this.appendReportItem(reportListElem, reportItem); 80 | 81 | const jsonName = `${attachmentName.split('.')[0]}.json`; 82 | this.taskClient 83 | .getAttachmentContent( 84 | this.projectId, 85 | BUILD_PHASE, 86 | this.planId, 87 | timelineId, 88 | recordId, 89 | JSON_ATTACHMENT_TYPE, 90 | jsonName, 91 | ) 92 | .then((content) => { 93 | const json = JSON.parse( 94 | new TextDecoder('utf-8').decode(new DataView(content)), 95 | ); 96 | this.improveReportDisplayName( 97 | attachmentName, 98 | json, 99 | reportItem, 100 | ); 101 | }); 102 | 103 | if (this.reportList.length == 1) this.reportList[0].click(); 104 | VSS.notifyLoadSucceeded(); 105 | } 106 | }); 107 | }); 108 | }); 109 | } 110 | }; 111 | 112 | private improveReportDisplayName = ( 113 | attachmentName: string, 114 | jsonResults: object | any[], 115 | reportItem: HTMLElement, 116 | ): void => { 117 | // TODO: should we fail in this case? Or is this a valid state? 118 | if (!jsonResults) { 119 | return; 120 | } 121 | 122 | const img: HTMLImageElement = document.createElement('img'); 123 | const span: HTMLElement = document.createElement('span'); 124 | const vulnsFound = detectVulns(jsonResults); 125 | const spanText = generateReportTitle(jsonResults, attachmentName); 126 | 127 | $(reportItem).addClass(vulnsFound ? 'failed' : 'passed'); 128 | img.src = vulnsFound 129 | ? '../img/report-failed.png' 130 | : '../img/report-passed.png'; 131 | $(img).addClass('reportImg'); 132 | $(span).text(spanText); 133 | reportItem.appendChild(img); 134 | reportItem.appendChild(span); 135 | }; 136 | 137 | private createReportItem = ( 138 | index: string | number | symbol, 139 | attachmentName: string, 140 | timelineId: string, 141 | recordId: string, 142 | ): HTMLElement => { 143 | const reportItem = document.createElement('li'); 144 | $(reportItem).addClass('report'); 145 | reportItem.onclick = () => 146 | this.showReport(index, attachmentName, timelineId, recordId); 147 | 148 | return reportItem; 149 | }; 150 | 151 | private appendReportItem = (list: HTMLElement, item: HTMLElement): void => { 152 | this.reportList.push(item); 153 | list.appendChild(item); 154 | }; 155 | 156 | private showReport = ( 157 | index: string | number | symbol, 158 | attachmentName: string, 159 | timelineId: string, 160 | recordId: string, 161 | ): void => { 162 | $.each(this.reportList, (index, reportItem) => 163 | $(reportItem).removeClass('currentReport'), 164 | ); 165 | $(this.reportList[index]).addClass('currentReport'); 166 | const content: string = '

Loading report...

'; 167 | this.fillReportIFrameContent(content); 168 | 169 | this.taskClient 170 | .getAttachmentContent( 171 | this.projectId, 172 | BUILD_PHASE, 173 | this.planId, 174 | timelineId, 175 | recordId, 176 | HTML_ATTACHMENT_TYPE, 177 | attachmentName, 178 | ) 179 | .then((content) => { 180 | const data = new TextDecoder('utf-8').decode(new DataView(content)); 181 | this.fillReportIFrameContent(data); 182 | }); 183 | }; 184 | 185 | private fillReportIFrameContent = (content: string): void => { 186 | (document.getElementById('iframeID') as HTMLDivElement).innerHTML = content; 187 | }; 188 | } 189 | 190 | SnykReportTab.enhance(SnykReportTab, $('#snyk-report'), {}); 191 | -------------------------------------------------------------------------------- /ui/enhancer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // revert to es6 /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "amd" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "../../scripts/dist" /* Redirect output structure to the directory. */, 6 | 7 | /* Strict Type-Checking Options */ 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 10 | "skipLibCheck": true, 11 | 12 | /* Module Resolution Options */ 13 | "types": [ 14 | /* Type declaration files to be included in compilation. */ 15 | "vss-web-extension-sdk", 16 | "jquery" 17 | ], 18 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 19 | 20 | /* Source Map Options */ 21 | "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */, 22 | "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 23 | }, 24 | "files": ["./snyk-report.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /ui/snyk-report-tab.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 58 | 59 | 75 | 76 | 77 | 78 | 81 |

Reports:

82 |
    83 |
    84 |
    85 |
    86 |
    87 | 88 | 89 | -------------------------------------------------------------------------------- /vss-extension-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "snyk-security-scan-dev", 4 | "name": "Snyk Security Scan (Dev)", 5 | "version": "0.2.2", 6 | "publisher": "snyk-dev", 7 | "targets": [ 8 | { 9 | "id": "Microsoft.VisualStudio.Services" 10 | } 11 | ], 12 | "description": "Snyk scan for open source vulnerabilities", 13 | "categories": ["Azure Pipelines"], 14 | "icons": { 15 | "default": "images/extension-icon.png" 16 | }, 17 | "scopes": ["vso.build_execute"], 18 | "files": [ 19 | { 20 | "path": "snykTask" 21 | }, 22 | { 23 | "path": "scripts/dist", 24 | "addressable": true, 25 | "packagePath": "scripts" 26 | }, 27 | { 28 | "path": "images", 29 | "addressable": true, 30 | "packagePath": "img" 31 | }, 32 | { 33 | "path": "ui/snyk-report-tab.html", 34 | "addressable": true 35 | }, 36 | { 37 | "path": "node_modules/vss-web-extension-sdk/lib", 38 | "addressable": true, 39 | "packagePath": "lib" 40 | }, 41 | { 42 | "path": "LICENSE", 43 | "addressable": true 44 | } 45 | ], 46 | "content": { 47 | "details": { 48 | "path": "marketplace.md" 49 | }, 50 | "license": { 51 | "path": "LICENSE" 52 | } 53 | }, 54 | "links": { 55 | "home": { 56 | "uri": "https://snyk.io/" 57 | }, 58 | "support": { 59 | "uri": "https://support.snyk.io/" 60 | }, 61 | "privacypolicy": { 62 | "uri": "https://snyk.io/policies/privacy/" 63 | }, 64 | "license": { 65 | "uri": "./LICENSE" 66 | } 67 | }, 68 | "contributions": [ 69 | { 70 | "id": "custom-build-release-task", 71 | "type": "ms.vss-distributed-task.task", 72 | "targets": ["ms.vss-distributed-task.tasks"], 73 | "properties": { 74 | "name": "snykTask" 75 | } 76 | }, 77 | { 78 | "id": "snyk-report-tab", 79 | "type": "ms.vss-build-web.build-results-tab", 80 | "description": "Snyk Report", 81 | "targets": ["ms.vss-build-web.build-results-view"], 82 | "properties": { 83 | "name": "Snyk Report", 84 | "uri": "ui/snyk-report-tab.html" 85 | } 86 | }, 87 | { 88 | "id": "snyk-service-connection-endpoint", 89 | "description": "Snyk.io", 90 | "type": "ms.vss-endpoint.service-endpoint-type", 91 | "targets": ["ms.vss-endpoint.endpoint-types"], 92 | "properties": { 93 | "name": "SnykAuth", 94 | "displayName": "Snyk Authentication", 95 | "url": "https://snyk.io/", 96 | "authenticationSchemes": [ 97 | { 98 | "type": "ms.vss-endpoint.endpoint-auth-scheme-token", 99 | "inputDescriptors": [ 100 | { 101 | "id": "apitoken", 102 | "name": "Snyk API Token", 103 | "description": "Log into your Snyk account to get either your Personal API Token or Service Account token.", 104 | "inputMode": "textbox", 105 | "isConfidential": true, 106 | "validation": { 107 | "isRequired": true, 108 | "dataType": "string" 109 | } 110 | } 111 | ] 112 | } 113 | ], 114 | "helpMarkDown": "Log into your Snyk account to get either your Personal API Token or Service Account token." 115 | } 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /vss-extension-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "preview-snyk-security-scan", 4 | "name": "(Preview) Snyk Security Scan", 5 | "version": "0.0.0", 6 | "publisher": "Snyk", 7 | "targets": [ 8 | { 9 | "id": "Microsoft.VisualStudio.Services" 10 | } 11 | ], 12 | "description": "Snyk scan for open source vulnerabilities", 13 | "categories": ["Azure Pipelines"], 14 | "icons": { 15 | "default": "images/extension-icon.png" 16 | }, 17 | "scopes": ["vso.build_execute"], 18 | "files": [ 19 | { 20 | "path": "snykTask" 21 | }, 22 | { 23 | "path": "scripts/dist", 24 | "addressable": true, 25 | "packagePath": "scripts" 26 | }, 27 | { 28 | "path": "images", 29 | "addressable": true, 30 | "packagePath": "img" 31 | }, 32 | { 33 | "path": "ui/snyk-report-tab.html", 34 | "addressable": true 35 | }, 36 | { 37 | "path": "node_modules/vss-web-extension-sdk/lib", 38 | "addressable": true, 39 | "packagePath": "lib" 40 | }, 41 | { 42 | "path": "LICENSE", 43 | "addressable": true 44 | } 45 | ], 46 | "content": { 47 | "details": { 48 | "path": "marketplace.md" 49 | }, 50 | "license": { 51 | "path": "LICENSE" 52 | } 53 | }, 54 | "links": { 55 | "home": { 56 | "uri": "https://snyk.io/" 57 | }, 58 | "support": { 59 | "uri": "https://support.snyk.io/" 60 | }, 61 | "privacypolicy": { 62 | "uri": "https://snyk.io/policies/privacy/" 63 | }, 64 | "license": { 65 | "uri": "./LICENSE" 66 | } 67 | }, 68 | "contributions": [ 69 | { 70 | "id": "custom-build-release-task", 71 | "type": "ms.vss-distributed-task.task", 72 | "targets": ["ms.vss-distributed-task.tasks"], 73 | "properties": { 74 | "name": "snykTask" 75 | } 76 | }, 77 | { 78 | "id": "snyk-report-tab", 79 | "type": "ms.vss-build-web.build-results-tab", 80 | "description": "Snyk Report", 81 | "targets": ["ms.vss-build-web.build-results-view"], 82 | "properties": { 83 | "name": "Snyk Report", 84 | "uri": "ui/snyk-report-tab.html" 85 | } 86 | } 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "snyk-security-scan", 4 | "name": "Snyk Security Scan", 5 | "version": "0.0.0", 6 | "publisher": "Snyk", 7 | "targets": [ 8 | { 9 | "id": "Microsoft.VisualStudio.Services" 10 | } 11 | ], 12 | "description": "Snyk scan for open source vulnerabilities", 13 | "categories": ["Azure Pipelines"], 14 | "icons": { 15 | "default": "images/extension-icon.png" 16 | }, 17 | "scopes": ["vso.build_execute"], 18 | "files": [ 19 | { 20 | "path": "snykTask" 21 | }, 22 | { 23 | "path": "scripts/dist", 24 | "addressable": true, 25 | "packagePath": "scripts" 26 | }, 27 | { 28 | "path": "images", 29 | "addressable": true, 30 | "packagePath": "img" 31 | }, 32 | { 33 | "path": "ui/snyk-report-tab.html", 34 | "addressable": true 35 | }, 36 | { 37 | "path": "node_modules/vss-web-extension-sdk/lib", 38 | "addressable": true, 39 | "packagePath": "lib" 40 | }, 41 | { 42 | "path": "LICENSE", 43 | "addressable": true 44 | } 45 | ], 46 | "content": { 47 | "details": { 48 | "path": "marketplace.md" 49 | }, 50 | "license": { 51 | "path": "LICENSE" 52 | } 53 | }, 54 | "links": { 55 | "home": { 56 | "uri": "https://snyk.io/" 57 | }, 58 | "support": { 59 | "uri": "https://support.snyk.io/" 60 | }, 61 | "privacypolicy": { 62 | "uri": "https://snyk.io/policies/privacy/" 63 | }, 64 | "license": { 65 | "uri": "./LICENSE" 66 | } 67 | }, 68 | "contributions": [ 69 | { 70 | "id": "custom-build-release-task", 71 | "type": "ms.vss-distributed-task.task", 72 | "targets": ["ms.vss-distributed-task.tasks"], 73 | "properties": { 74 | "name": "snykTask" 75 | } 76 | }, 77 | { 78 | "id": "snyk-report-tab", 79 | "type": "ms.vss-build-web.build-results-tab", 80 | "description": "Snyk Report", 81 | "targets": ["ms.vss-build-web.build-results-view"], 82 | "properties": { 83 | "name": "Snyk Report", 84 | "uri": "ui/snyk-report-tab.html" 85 | } 86 | }, 87 | { 88 | "id": "snyk-service-connection-endpoint", 89 | "description": "Snyk.io", 90 | "type": "ms.vss-endpoint.service-endpoint-type", 91 | "targets": ["ms.vss-endpoint.endpoint-types"], 92 | "properties": { 93 | "name": "SnykAuth", 94 | "displayName": "Snyk Authentication", 95 | "url": "https://snyk.io/", 96 | "authenticationSchemes": [ 97 | { 98 | "type": "ms.vss-endpoint.endpoint-auth-scheme-token", 99 | "inputDescriptors": [ 100 | { 101 | "id": "apitoken", 102 | "name": "Snyk API Token", 103 | "description": "Log into your Snyk account to get either your Personal API Token or Service Account token.", 104 | "inputMode": "textbox", 105 | "isConfidential": true, 106 | "validation": { 107 | "isRequired": true, 108 | "dataType": "string" 109 | } 110 | } 111 | ] 112 | } 113 | ], 114 | "helpMarkDown": "Log into your Snyk account to get either your Personal API Token or Service Account token." 115 | } 116 | } 117 | ] 118 | } 119 | --------------------------------------------------------------------------------