diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 89b1454..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,77 +0,0 @@ -version: 2.1 -orbs: - snyk: snyk/snyk@1.1.2 - -jobs: - build-test-monitor: - docker: - # specify the version - - image: circleci/node:latest - - steps: - - checkout - - run: npm install semantic-release @semantic-release/exec --save-dev - - run: - name: "Install deps" - command: | - npm install - - run: - name: "Run Tests" - command: | - npm test - - snyk/scan: - fail-on-issues: true - monitor-on-build: true - token-variable: SNYK_TOKEN - - run: npx semantic-release - - build-test: - docker: - # specify the version - - image: circleci/node:latest - - steps: - - checkout - - run: - name: "Install deps" - command: | - npm install - - run: - name: "Run Tests" - command: | - npm test - - snyk/scan: - fail-on-issues: true - monitor-on-build: false - token-variable: SNYK_TOKEN - -workflows: - version: 2.1 - nightly: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - jobs: - - build-test-monitor: - context: SNYK - build-test-monitor-publish: - jobs: - - build-test-monitor: - context: SNYK - filters: - branches: - only: - - master - build-test: - jobs: - - build-test: - context: SNYK - filters: - branches: - ignore: - - master - diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2bf8b65..b446cd1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -# Snyk Tech Services will be required for a review on every PR -* @snyk-tech-services/snyk-tech-services +# CS Engineering will be required for a review on every PR +* @snyk-labs/cs-engineers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..847fb8b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +# This is a basic workflow to help you get started with Actions + +name: ci + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "main" branch + push: + branches: + - '**' + pull_request: + branches: + - 'master' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build-test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: | + npm install + - name: Run tests + run: | + npm test + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + build-test-monitor: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + needs: build-test + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: | + npm install semantic-release @semantic-release/exec pkg --save-dev + npm install + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --org=cse-snyk-labs + command: monitor diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4ebab2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release action +on: + push: + branches: + - master + +permissions: + contents: read # for checkout + +jobs: + build-and-publish: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + - name: Install dependencies + run: npm install + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release diff --git a/package.json b/package.json index 1e7f1ea..f3ea4bf 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "homepage": "https://github.com/snyk-tech-services/snyk-request-manager#readme", "dependencies": { "@snyk/configstore": "^3.2.0-rc1", + "@types/babel__traverse": "7.17.1", "@types/debug": "^4.1.7", "@types/uuid": "^7.0.3", "axios": "0.27.2", @@ -41,15 +42,18 @@ "global-agent": "3.0.0", "leaky-bucket-queue": "0.0.2", "lodash": "4.17.21", + "proxy-from-env": "^1.1.0", "snyk-config": "^5.0.1", "source-map-support": "^0.5.16", "tslib": "^1.10.0", "uuid": "^8.0.0" }, "devDependencies": { + "@types/global-agent": "^2.1.1", "@types/jest": "^25.1.1", "@types/lodash": "4.14.186", "@types/node": "^12.12.26", + "@types/proxy-from-env": "^1.0.2", "@typescript-eslint/eslint-plugin": "^2.18.0", "@typescript-eslint/parser": "^2.18.0", "eslint": "^6.8.0", diff --git a/src/lib/request/request.ts b/src/lib/request/request.ts index 7724aa2..0803f50 100644 --- a/src/lib/request/request.ts +++ b/src/lib/request/request.ts @@ -3,7 +3,8 @@ import * as Error from '../customErrors/apiError'; // Fixes issue https://github.com/axios/axios/issues/3384 // where HTTPS over HTTP Proxy Fails with 500 handshakefailed on mcafee proxy -import 'global-agent/bootstrap'; +import { bootstrap } from 'global-agent'; +import { getProxyForUrl } from 'proxy-from-env'; const DEFAULT_API = 'https://snyk.io/api/v1'; const DEFAULT_REST_API = 'https://api.snyk.io/rest/'; @@ -16,6 +17,16 @@ interface SnykRequest { useRESTApi?: boolean; } +if (process.env.HTTP_PROXY || process.env.http_proxy) { + process.env.HTTP_PROXY = process.env.HTTP_PROXY || process.env.http_proxy; +} +if (process.env.HTTPS_PROXY || process.env.https_proxy) { + process.env.HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.https_proxy; +} +if (process.env.NP_PROXY || process.env.no_proxy) { + process.env.NO_PROXY = process.env.NO_PROXY || process.env.no_proxy; +} + const getTopParentModuleName = (parent: NodeModule | null): string => { if (parent == null) { return ''; @@ -35,6 +46,12 @@ const makeSnykRequest = async ( apiUrlREST = DEFAULT_REST_API, userAgentPrefix = '', ): Promise> => { + const proxyUri = getProxyForUrl(request.useRESTApi ? apiUrlREST : apiUrl); + if (proxyUri) { + bootstrap({ + environmentVariableNamespace: '', + }); + } const topParentModuleName = getTopParentModuleName(module.parent as any); const userAgentPrefixChecked = userAgentPrefix != '' && !userAgentPrefix.endsWith('/') @@ -48,15 +65,34 @@ const makeSnykRequest = async ( Authorization: 'token ' + snykToken, 'User-Agent': `${topParentModuleName}${userAgentPrefixChecked}tech-services/snyk-request-manager/1.0`, }; + let apiClient; + if (proxyUri) { + apiClient = axios.create({ + baseURL: request.useRESTApi ? apiUrlREST : apiUrl, + responseType: 'json', + headers: { ...requestHeaders, ...request.headers }, + transitional: { + clarifyTimeoutError: true, + }, + timeout: 30_000, // 5 mins same as Snyk APIs + proxy: false, // disables axios built-in proxy to let bootstrap work + }); + } else { + apiClient = axios.create({ + baseURL: request.useRESTApi ? apiUrlREST : apiUrl, + responseType: 'json', + headers: { ...requestHeaders, ...request.headers }, + transitional: { + clarifyTimeoutError: true, + }, + timeout: 30_000, // 5 mins same as Snyk APIs + }); + } - const apiClient = axios.create({ - baseURL: request.useRESTApi ? apiUrlREST : apiUrl, - responseType: 'json', - headers: { ...requestHeaders, ...request.headers }, - transitional: { - clarifyTimeoutError: true, - }, - timeout: 30_000, // 5 mins same as Snyk APIs + // sanitize error to avoid leaking sensitive data + apiClient.interceptors.response.use(undefined, async (error) => { + error.config.headers.Authorization = '****'; + return Promise.reject(error); }); try { diff --git a/test/lib/request/request.test.ts b/test/lib/request/request.test.ts index 2796d12..4685286 100644 --- a/test/lib/request/request.test.ts +++ b/test/lib/request/request.test.ts @@ -30,6 +30,9 @@ beforeEach(() => { .reply(512, '512') .post(/\/genericerror/) .reply(512, '512') + .get(/\/gotimeout/) + .delayConnection(32000) + .reply(504, '504') .get(/\/apiautherror/) .reply(401, '401') .post(/\/apiautherror/) @@ -206,4 +209,23 @@ describe('Test Snyk Utils error handling/classification', () => { expect(err).toBeInstanceOf(GenericError); } }); + + it('Test Timeout error on GET command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'GET', + url: '/gotimeout', + body: JSON.stringify(bodyToSend), + }, + 'token123', + ); + } catch (err) { + expect(err).toBeInstanceOf(GenericError); + expect(err.message.config.headers.Authorization).toBe('****'); + } + }); }); diff --git a/test/lib/request/rest-request.test.ts b/test/lib/request/rest-request.test.ts index 204e608..be9c3bf 100644 --- a/test/lib/request/rest-request.test.ts +++ b/test/lib/request/rest-request.test.ts @@ -32,6 +32,9 @@ beforeEach(() => { .reply(512, '512') .post(/\/genericerror/) .reply(512, '512') + .get(/\/gotimeout/) + .delayConnection(32000) + .reply(504, '504') .get(/\/apiautherror/) .reply(401, '401') .post(/\/apiautherror/) @@ -287,4 +290,24 @@ describe('Test Snyk Utils error handling/classification', () => { expect(err).toBeInstanceOf(GenericError); } }); + + it('Test Timeout error on GET command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'GET', + url: '/gotimeout', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + } catch (err) { + expect(err).toBeInstanceOf(GenericError); + expect(err.message.config.headers.Authorization).toBe('****'); + } + }); });