diff --git a/CHANGELOG.md b/CHANGELOG.md index c0497d6ef..05f47961d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Snyk Security - Code and Open Source Dependencies Changelog +## [1.15.3] + +### Changed + +- Extension uses Language Server to run Snyk Code scans. + ## [1.15.2] ### Fixed @@ -16,7 +22,6 @@ ### Changed -- Extension uses Language Server to run Snyk Code scans. - Snyk Code "Advanced" menu replaced with a settings option called "Scanning Mode". ## [1.14.0] diff --git a/package-lock.json b/package-lock.json index 5e884fb78..6c814e180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,13 @@ "@babel/parser": "^7.12.11", "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", - "@deepcode/dcignore": "^1.0.4", "@itly/plugin-amplitude-node": "^2.5.0", "@itly/plugin-schema-validator": "^2.4.0", "@itly/plugin-segment-node": "^2.4.0", "@itly/sdk": "^2.3.1", "@sentry/node": "^6.16.1", "@sentry/tracing": "^6.19.7", + "@snyk/code-client": "^4.12.4", "analytics-node": "^4.0.1", "axios": "^0.27.2", "glob": "^7.2.0", @@ -656,7 +656,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.4", "run-parallel": "^1.1.9" @@ -669,7 +668,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "dev": true, "engines": { "node": ">= 8" } @@ -678,7 +676,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.4", "fastq": "^1.6.0" @@ -1320,7 +1317,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-1.0.4.tgz", "integrity": "sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==", - "deprecated": "Deprecated in favor of @oclif/core", "dev": true, "engines": { "node": ">=8.0.0" @@ -1544,6 +1540,30 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "node_modules/@snyk/code-client": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@snyk/code-client/-/code-client-4.12.4.tgz", + "integrity": "sha512-K2k8J761iJfKxWI026ZAWw15Xaw3Vff4Wmy9SPx5QR3aU6qrHn1d872v+9B8nnrHfbgJbU45t/iLzCv/fiotNg==", + "dependencies": { + "@deepcode/dcignore": "^1.0.4", + "@types/flat-cache": "^2.0.0", + "@types/lodash.omit": "^4.5.6", + "@types/lodash.pick": "^4.4.6", + "@types/lodash.union": "^4.6.6", + "@types/sarif": "^2.1.4", + "@types/uuid": "^8.3.1", + "fast-glob": "^3.2.11", + "ignore": "^5.1.8", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "lodash.union": "^4.6.0", + "multimatch": "^5.0.0", + "needle": "~3.0.0", + "p-map": "^3.0.0", + "uuid": "^8.3.2", + "yaml": "^1.10.2" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1605,6 +1625,11 @@ "@types/node": "*" } }, + "node_modules/@types/flat-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/flat-cache/-/flat-cache-2.0.0.tgz", + "integrity": "sha512-fHeEsm9hvmZ+QHpw6Fkvf19KIhuqnYLU6vtWLjd5BsMd/qVi7iTkMioDZl0mQmfNRA1A6NwvhrSRNr9hGYZGww==" + }, "node_modules/@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -1639,8 +1664,31 @@ "node_modules/@types/lodash": { "version": "4.14.168", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", - "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", - "dev": true + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + }, + "node_modules/@types/lodash.omit": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.6.tgz", + "integrity": "sha512-KXPpOSNX2h0DAG2w7ajpk7TXvWF28ZHs5nJhOJyP0BQHkehgr948RVsToItMme6oi0XJkp19CbuNXkIX8FiBlQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.pick": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/lodash.pick/-/lodash.pick-4.4.6.tgz", + "integrity": "sha512-u8bzA16qQ+8dY280z3aK7PoWb3fzX5ATJ0rJB6F+uqchOX2VYF02Aqa+8aYiHiHgPzQiITqCgeimlyKFy4OA6g==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.union": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.union/-/lodash.union-4.6.6.tgz", + "integrity": "sha512-Wu0ZEVNcyCz8eAn6TlUbYWZoGbH9E+iOHxAZbwUoCEXdUiy6qpcz5o44mMXViM4vlPLLCPlkAubEP1gokoSZaw==", + "dependencies": { + "@types/lodash": "*" + } }, "node_modules/@types/marked": { "version": "3.0.0", @@ -1651,8 +1699,7 @@ "node_modules/@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", - "dev": true + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, "node_modules/@types/mocha": { "version": "8.2.3", @@ -1675,6 +1722,11 @@ "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", "dev": true }, + "node_modules/@types/sarif": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.4.tgz", + "integrity": "sha512-4xKHMdg3foh3Va1fxTzY1qt8QVqmaJpGWsVvtjQrJBn+/bkig2pWFKJ4FPI2yLI4PAj0SUKiPO4Vd7ggYIMZjQ==" + }, "node_modules/@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", @@ -1708,8 +1760,7 @@ "node_modules/@types/uuid": { "version": "8.3.1", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", - "dev": true + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==" }, "node_modules/@types/validate-npm-package-name": { "version": "3.0.3", @@ -2035,6 +2086,26 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2182,6 +2253,14 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "engines": { + "node": ">=8" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2211,7 +2290,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -2242,6 +2320,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2407,7 +2493,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -4011,7 +4096,6 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4039,7 +4123,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -4084,7 +4167,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4368,7 +4450,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4660,7 +4741,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -4692,7 +4772,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, "engines": { "node": ">= 4" } @@ -4741,7 +4820,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, "engines": { "node": ">=8" } @@ -5018,7 +5096,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5036,7 +5113,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -5069,7 +5145,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -5385,6 +5460,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -5404,6 +5489,11 @@ "lodash._reinterpolate": "^3.0.0" } }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, "node_modules/log-chopper": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/log-chopper/-/log-chopper-1.0.2.tgz", @@ -5523,7 +5613,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -5541,7 +5630,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -5827,6 +5915,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -5860,6 +5966,30 @@ "node": "*" } }, + "node_modules/needle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.0.0.tgz", + "integrity": "sha512-eGr0qnfHxAjr+Eptl1zr2lgUQUPC1SZfTkg2kFi0kxr1ChJonHUVYobkug8siBKMlyUVVp56MSkp6CSeXH/jgw==", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -6257,6 +6387,17 @@ "node": ">=4" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", @@ -6454,7 +6595,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -6723,7 +6863,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -6905,7 +7044,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6939,7 +7077,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -6994,8 +7131,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.48.0", @@ -7014,6 +7150,11 @@ "node": ">=8.9.0" } }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -7498,7 +7639,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -8155,6 +8295,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -8702,7 +8850,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.4", "run-parallel": "^1.1.9" @@ -8711,14 +8858,12 @@ "@nodelib/fs.stat": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "dev": true + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" }, "@nodelib/fs.walk": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.4", "fastq": "^1.6.0" @@ -9416,6 +9561,30 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@snyk/code-client": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@snyk/code-client/-/code-client-4.12.4.tgz", + "integrity": "sha512-K2k8J761iJfKxWI026ZAWw15Xaw3Vff4Wmy9SPx5QR3aU6qrHn1d872v+9B8nnrHfbgJbU45t/iLzCv/fiotNg==", + "requires": { + "@deepcode/dcignore": "^1.0.4", + "@types/flat-cache": "^2.0.0", + "@types/lodash.omit": "^4.5.6", + "@types/lodash.pick": "^4.4.6", + "@types/lodash.union": "^4.6.6", + "@types/sarif": "^2.1.4", + "@types/uuid": "^8.3.1", + "fast-glob": "^3.2.11", + "ignore": "^5.1.8", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "lodash.union": "^4.6.0", + "multimatch": "^5.0.0", + "needle": "~3.0.0", + "p-map": "^3.0.0", + "uuid": "^8.3.2", + "yaml": "^1.10.2" + } + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -9474,6 +9643,11 @@ "@types/node": "*" } }, + "@types/flat-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/flat-cache/-/flat-cache-2.0.0.tgz", + "integrity": "sha512-fHeEsm9hvmZ+QHpw6Fkvf19KIhuqnYLU6vtWLjd5BsMd/qVi7iTkMioDZl0mQmfNRA1A6NwvhrSRNr9hGYZGww==" + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -9508,8 +9682,31 @@ "@types/lodash": { "version": "4.14.168", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", - "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", - "dev": true + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + }, + "@types/lodash.omit": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.6.tgz", + "integrity": "sha512-KXPpOSNX2h0DAG2w7ajpk7TXvWF28ZHs5nJhOJyP0BQHkehgr948RVsToItMme6oi0XJkp19CbuNXkIX8FiBlQ==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.pick": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/lodash.pick/-/lodash.pick-4.4.6.tgz", + "integrity": "sha512-u8bzA16qQ+8dY280z3aK7PoWb3fzX5ATJ0rJB6F+uqchOX2VYF02Aqa+8aYiHiHgPzQiITqCgeimlyKFy4OA6g==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.union": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.union/-/lodash.union-4.6.6.tgz", + "integrity": "sha512-Wu0ZEVNcyCz8eAn6TlUbYWZoGbH9E+iOHxAZbwUoCEXdUiy6qpcz5o44mMXViM4vlPLLCPlkAubEP1gokoSZaw==", + "requires": { + "@types/lodash": "*" + } }, "@types/marked": { "version": "3.0.0", @@ -9520,8 +9717,7 @@ "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", - "dev": true + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, "@types/mocha": { "version": "8.2.3", @@ -9544,6 +9740,11 @@ "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", "dev": true }, + "@types/sarif": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.4.tgz", + "integrity": "sha512-4xKHMdg3foh3Va1fxTzY1qt8QVqmaJpGWsVvtjQrJBn+/bkig2pWFKJ4FPI2yLI4PAj0SUKiPO4Vd7ggYIMZjQ==" + }, "@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", @@ -9577,8 +9778,7 @@ "@types/uuid": { "version": "8.3.1", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", - "dev": true + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==" }, "@types/validate-npm-package-name": { "version": "3.0.3", @@ -9774,6 +9974,22 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + } + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -9890,6 +10106,11 @@ "sprintf-js": "~1.0.2" } }, + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -9912,8 +10133,7 @@ "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, "array-uniq": { "version": "1.0.2", @@ -9932,6 +10152,11 @@ "es-abstract": "^1.19.0" } }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10069,7 +10294,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -11288,7 +11512,6 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -11313,7 +11536,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, "requires": { "reusify": "^1.0.4" } @@ -11346,7 +11568,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -11566,7 +11787,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -11768,7 +11988,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -11782,8 +12001,7 @@ "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" }, "ignore-walk": { "version": "3.0.3", @@ -11819,8 +12037,7 @@ "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, "inflight": { "version": "1.0.6", @@ -12023,8 +12240,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -12036,7 +12252,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -12056,8 +12271,7 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-number-object": { "version": "1.0.6", @@ -12298,6 +12512,16 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=" + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -12317,6 +12541,11 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, "log-chopper": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/log-chopper/-/log-chopper-1.0.2.tgz", @@ -12410,8 +12639,7 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "methods": { "version": "1.1.2", @@ -12423,7 +12651,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -12622,6 +12849,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -12646,6 +12885,26 @@ "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", "dev": true }, + "needle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.0.0.tgz", + "integrity": "sha512-eGr0qnfHxAjr+Eptl1zr2lgUQUPC1SZfTkg2kFi0kxr1ChJonHUVYobkug8siBKMlyUVVp56MSkp6CSeXH/jgw==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -12951,6 +13210,14 @@ "p-limit": "^1.1.0" } }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", @@ -13109,8 +13376,7 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pkce-challenge": { "version": "2.2.0", @@ -13308,8 +13574,7 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "randombytes": { "version": "2.1.0", @@ -13444,8 +13709,7 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rimraf": { "version": "3.0.2", @@ -13466,7 +13730,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -13495,8 +13758,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { "version": "1.48.0", @@ -13509,6 +13771,11 @@ "source-map-js": ">=0.6.2 <2.0.0" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -13905,7 +14172,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -14415,6 +14681,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 60ca65514..37c8d11f5 100644 --- a/package.json +++ b/package.json @@ -205,6 +205,18 @@ "description": "Preview features that are currently in development. Setting keys will be removed when features become stable.", "propertyNames": true, "properties": { + "lsCode": { + "type": "boolean", + "title": "Enable \"Code\" using Snyk Language Server", + "description": "Enables Snyk Code results delivery using Snyk Language Server.", + "default": false + }, + "reportFalsePositives": { + "type": "boolean", + "title": "Enable \"report false positives\"", + "description": "Allows reporting false positives for Snyk Code suggestions.", + "default": false + }, "advisor": { "type": "boolean", "title": "Enable \"Snyk Advisor\"", @@ -244,14 +256,34 @@ "when": "snyk:loggedIn && snyk:featuresSelected && snyk:workspaceFound && !snyk:error" }, { - "id": "snyk.views.analysis.code.security", + "id": "snyk.views.analysis.code.security.old", "name": "Code Security", - "when": "snyk:loggedIn && snyk:featuresSelected && snyk:workspaceFound && !snyk:error" + "when": "snyk:loggedIn && snyk:featuresSelected && snyk:codeEnabled && !snyk:codeLocalEngineEnabled && snyk:workspaceFound && !snyk:error" }, { - "id": "snyk.views.analysis.code.quality", + "id": "snyk.views.analysis.code.quality.old", "name": "Code Quality", - "when": "snyk:loggedIn && snyk:featuresSelected && snyk:workspaceFound && !snyk:error" + "when": "snyk:loggedIn && snyk:featuresSelected && snyk:codeEnabled && !snyk:codeLocalEngineEnabled && snyk:workspaceFound && !snyk:error" + }, + { + "id": "snyk.views.analysis.code.security", + "name": "Code Security (LS)", + "when": "snyk:lsCodePreview && snyk:loggedIn && snyk:featuresSelected && snyk:codeEnabled && !snyk:codeLocalEngineEnabled && snyk:workspaceFound && !snyk:error" + }, + { + "id": "snyk.views.analysis.code.quality", + "name": "Code Quality (LS)", + "when": "snyk:lsCodePreview && snyk:loggedIn && snyk:featuresSelected && snyk:codeEnabled && !snyk:codeLocalEngineEnabled && snyk:workspaceFound && !snyk:error" + }, + { + "id": "snyk.views.analysis.code.enablement", + "name": "Code Security & Quality", + "when": "snyk:loggedIn && snyk:featuresSelected && !snyk:codeEnabled && snyk:workspaceFound && !snyk:error" + }, + { + "id": "snyk.views.analysis.code.localEngine", + "name": "Code Security & Quality", + "when": "snyk:loggedIn && snyk:featuresSelected && snyk:codeEnabled && snyk:codeLocalEngineEnabled && snyk:workspaceFound && !snyk:error" }, { "id": "snyk.views.support", @@ -275,6 +307,14 @@ "contents": "We are now redirecting you to our auth page, go ahead and log in. If a browser window doesn't open after a few seconds, please copy the url below and manually paste it in a browser.\n[Copy URL to clipboard](command:snyk.copyAuthLink 'Copy URL to clipboard')", "when": "!snyk:error && !snyk:loggedIn && snyk:authenticating" }, + { + "view": "snyk.views.analysis.code.enablement", + "contents": "Thanks for connecting with Snyk. ✅\n 👉 You are almost set 🤗.\n[Enable Snyk Code and start analysing](command:snyk.enableCode 'Upload code to Snyk')\nIt looks like your organization's configuration is disabled, that's why you are seeing this message. You can easily enable it by pressing the above button and switching it on.\nWe apologize for the inconvenience and please [contact us](https://snyk.io/contact-us/?utm_source=vsc) if you have any other questions or concerns!" + }, + { + "view": "snyk.views.analysis.code.localEngine", + "contents": "Snyk Code is configured to use a Local Code Engine instance. This setup is not yet supported by the extension." + }, { "view": "snyk.views.welcome", "contents": "Open a workspace or a folder in Visual Studio Code to start the analysis.", @@ -285,12 +325,12 @@ "view/title": [ { "command": "snyk.start", - "when": "view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.oss'", + "when": "view == 'snyk.views.analysis.code.security.old' || view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.quality.old' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.oss'", "group": "navigation" }, { "command": "snyk.settings", - "when": "view == 'snyk.views.analysis.code.security.old' || view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.quality.old' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.welcome' || view == 'snyk.views.actions'", + "when": "view == 'snyk.views.analysis.code.security.old' || view == 'snyk.views.analysis.code.security' || view == 'snyk.views.analysis.code.quality.old' || view == 'snyk.views.analysis.code.quality' || view == 'snyk.views.analysis.oss' || view == 'snyk.views.welcome'", "group": "navigation" } ], @@ -305,7 +345,7 @@ }, { "command": "snyk.dcignore", - "when": "!snyk:error && snyk:loggedIn && snyk:workspaceFound" + "when": "!snyk:error && snyk:loggedIn && snyk:codeEnabled && !snyk:codeLocalEngineEnabled && snyk:workspaceFound" } ] }, @@ -413,13 +453,13 @@ "@babel/parser": "^7.12.11", "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", - "@deepcode/dcignore": "^1.0.4", "@itly/plugin-amplitude-node": "^2.5.0", "@itly/plugin-schema-validator": "^2.4.0", "@itly/plugin-segment-node": "^2.4.0", "@itly/sdk": "^2.3.1", "@sentry/node": "^6.16.1", "@sentry/tracing": "^6.19.7", + "@snyk/code-client": "^4.12.4", "analytics-node": "^4.0.1", "axios": "^0.27.2", "glob": "^7.2.0", @@ -435,4 +475,4 @@ "validate-npm-package-name": "^3.0.0", "vscode-languageclient": "8.0.0-next.2" } -} \ No newline at end of file +} diff --git a/src/snyk/base/modules/baseSnykModule.ts b/src/snyk/base/modules/baseSnykModule.ts index 7a3647556..cd77da048 100644 --- a/src/snyk/base/modules/baseSnykModule.ts +++ b/src/snyk/base/modules/baseSnykModule.ts @@ -13,6 +13,7 @@ import { ContextService, IContextService } from '../../common/services/contextSe import { DownloadService } from '../../common/services/downloadService'; import { LearnService } from '../../common/services/learnService'; import { INotificationService } from '../../common/services/notificationService'; +import { IOpenerService, OpenerService } from '../../common/services/openerService'; import { IViewManagerService, ViewManagerService } from '../../common/services/viewManagerService'; import { User } from '../../common/user'; import { CodeActionKindAdapter, ICodeActionKindAdapter } from '../../common/vscode/codeAction'; @@ -21,9 +22,15 @@ import { IMarkdownStringAdapter, MarkdownStringAdapter } from '../../common/vsco import { vsCodeWorkspace } from '../../common/vscode/workspace'; import { IWatcher } from '../../common/watchers/interfaces'; import { ISnykCodeService } from '../../snykCode/codeService'; +import { ISnykCodeServiceOld } from '../../snykCode/codeServiceOld'; +import { CodeSettings, ICodeSettings } from '../../snykCode/codeSettings'; +import { ISnykCodeErrorHandler, SnykCodeErrorHandler } from '../../snykCode/error/snykCodeErrorHandler'; +import { FalsePositiveApi, IFalsePositiveApi } from '../../snykCode/falsePositive/api/falsePositiveApi'; +import SnykEditorsWatcher from '../../snykCode/watchers/editorsWatcher'; import { OssService } from '../../snykOss/services/ossService'; import { OssVulnerabilityCountService } from '../../snykOss/services/vulnerabilityCount/ossVulnerabilityCountService'; import { IAuthenticationService } from '../services/authenticationService'; +import { ScanModeService } from '../services/scanModeService'; import SnykStatusBarItem, { IStatusBarItem } from '../statusBarItem/statusBarItem'; import { ILoadingBadge, LoadingBadge } from '../views/loadingBadge'; import { IBaseSnykModule } from './interfaces'; @@ -33,9 +40,11 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { readonly statusBarItem: IStatusBarItem; + protected readonly editorsWatcher: IWatcher; protected configurationWatcher: IWatcher; readonly contextService: IContextService; + readonly openerService: IOpenerService; readonly viewManagerService: IViewManagerService; protected authService: IAuthenticationService; protected downloadService: DownloadService; @@ -43,6 +52,7 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { protected advisorService?: AdvisorProvider; protected learnService: LearnService; protected commandController: CommandController; + protected scanModeService: ScanModeService; protected ossVulnerabilityCountService: OssVulnerabilityCountService; protected advisorScoreDisposable: AdvisorService; protected languageServer: ILanguageServer; @@ -52,11 +62,15 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { protected snykApiClient: ISnykApiClient; protected advisorApiClient: IAdvisorApiClient; + protected falsePositiveApi: IFalsePositiveApi; + snykCodeOld: ISnykCodeServiceOld; snykCode: ISnykCodeService; + protected codeSettings: ICodeSettings; readonly loadingBadge: ILoadingBadge; protected user: User; protected experimentService: ExperimentService; + protected snykCodeErrorHandler: ISnykCodeErrorHandler; protected markdownStringAdapter: IMarkdownStringAdapter; readonly workspaceTrust: IWorkspaceTrust; @@ -64,11 +78,22 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { constructor() { this.statusBarItem = new SnykStatusBarItem(); + this.editorsWatcher = new SnykEditorsWatcher(); this.viewManagerService = new ViewManagerService(); this.contextService = new ContextService(); + this.openerService = new OpenerService(); this.loadingBadge = new LoadingBadge(); this.learnService = new LearnService(configuration, Logger); this.snykApiClient = new SnykApiClient(configuration, vsCodeWorkspace, Logger); + this.falsePositiveApi = new FalsePositiveApi(configuration, vsCodeWorkspace, Logger); + this.snykCodeErrorHandler = new SnykCodeErrorHandler( + this.contextService, + this.loadingBadge, + Logger, + this, + configuration, + ); + this.codeSettings = new CodeSettings(this.snykApiClient, this.contextService, configuration, this.openerService); this.advisorApiClient = new AdvisorApiClient(configuration, Logger); this.markdownStringAdapter = new MarkdownStringAdapter(); this.workspaceTrust = new WorkspaceTrust(); @@ -77,5 +102,7 @@ export default abstract class BaseSnykModule implements IBaseSnykModule { abstract runScan(): Promise; + abstract runCodeScan(): Promise; + abstract runOssScan(): Promise; } diff --git a/src/snyk/base/modules/interfaces.ts b/src/snyk/base/modules/interfaces.ts index ddf5c3b06..b78fabb1c 100644 --- a/src/snyk/base/modules/interfaces.ts +++ b/src/snyk/base/modules/interfaces.ts @@ -1,8 +1,10 @@ import { IWorkspaceTrust } from '../../common/configuration/trustedFolders'; import { IContextService } from '../../common/services/contextService'; +import { IOpenerService } from '../../common/services/openerService'; import { IViewManagerService } from '../../common/services/viewManagerService'; import { ExtensionContext } from '../../common/vscode/extensionContext'; import { ExtensionContext as VSCodeExtensionContext } from '../../common/vscode/types'; +import { ISnykCodeServiceOld } from '../../snykCode/codeServiceOld'; import { IStatusBarItem } from '../statusBarItem/statusBarItem'; import { ILoadingBadge } from '../views/loadingBadge'; @@ -10,15 +12,23 @@ export interface IBaseSnykModule { readonly loadingBadge: ILoadingBadge; statusBarItem: IStatusBarItem; contextService: IContextService; + openerService: IOpenerService; viewManagerService: IViewManagerService; + snykCodeOld: ISnykCodeServiceOld; readonly workspaceTrust: IWorkspaceTrust; // Abstract methods runScan(): Promise; + runCodeScan(manual?: boolean): Promise; runOssScan(manual?: boolean): Promise; } -export interface IExtension extends IBaseSnykModule { +export interface ISnykLib { + enableCode(): Promise; + checkAdvancedMode(): Promise; +} + +export interface IExtension extends IBaseSnykModule, ISnykLib { context: ExtensionContext | undefined; activate(context: VSCodeExtensionContext): void; restartLanguageServer(): Promise; diff --git a/src/snyk/base/modules/snykLib.ts b/src/snyk/base/modules/snykLib.ts index 680ebc277..cd40a7f05 100644 --- a/src/snyk/base/modules/snykLib.ts +++ b/src/snyk/base/modules/snykLib.ts @@ -9,10 +9,14 @@ import { ErrorHandler } from '../../common/error/errorHandler'; import { Logger } from '../../common/logger/logger'; import { vsCodeWorkspace } from '../../common/vscode/workspace'; import BaseSnykModule from './baseSnykModule'; +import { ISnykLib } from './interfaces'; -export default class SnykLib extends BaseSnykModule { +export default class SnykLib extends BaseSnykModule implements ISnykLib { private async runFullScan_(manual = false): Promise { + Logger.info('Starting full scan'); + await this.contextService.setContext(SNYK_CONTEXT.ERROR, false); + this.snykCodeErrorHandler.resetTransientErrors(); this.loadingBadge.setLoadingBadge(false); const token = await configuration.getToken(); @@ -40,6 +44,7 @@ export default class SnykLib extends BaseSnykModule { this.logFullAnalysisIsTriggered(manual); void this.startOssAnalysis(manual, false); + await this.startSnykCodeAnalysis(workspacePaths, manual, false); // mark void, handle errors inside of startSnykCodeAnalysis() } } catch (err) { await ErrorHandler.handleGlobal(err, Logger, this.contextService, this.loadingBadge); @@ -50,8 +55,51 @@ export default class SnykLib extends BaseSnykModule { // We should avoid having duplicate parallel executions. public runScan = _.debounce(this.runFullScan_.bind(this), DEFAULT_SCAN_DEBOUNCE_INTERVAL, { leading: true }); + public runCodeScan = _.debounce(this.startSnykCodeAnalysis.bind(this), DEFAULT_SCAN_DEBOUNCE_INTERVAL, { + leading: true, + }); + public runOssScan = _.debounce(this.startOssAnalysis.bind(this), OSS_SCAN_DEBOUNCE_INTERVAL, { leading: true }); + async enableCode(): Promise { + const wasEnabled = await this.codeSettings.enable(); + if (wasEnabled) { + await this.codeSettings.checkCodeEnabled(); + + Logger.info('Snyk Code was enabled.'); + try { + await this.startSnykCodeAnalysis(); + } catch (err) { + ErrorHandler.handle(err, Logger); + } + } + } + + async startSnykCodeAnalysis(paths: string[] = [], manual = false, reportTriggeredEvent = true): Promise { + // If the execution is suspended, we only allow user-triggered Snyk Code analyses. + if (this.isSnykCodeAutoscanSuspended(manual)) { + return; + } + + const codeEnabled = await this.codeSettings.checkCodeEnabled(); + if (!codeEnabled) { + return; + } + + if ( + !configuration.getFeaturesConfiguration()?.codeSecurityEnabled && + !configuration.getFeaturesConfiguration()?.codeQualityEnabled + ) { + return; + } + + if (!paths.length) { + paths = vsCodeWorkspace.getWorkspaceFolders(); + } + + await this.snykCodeOld.startAnalysis(paths, manual, reportTriggeredEvent); + } + async onDidChangeWelcomeViewVisibility(visible: boolean): Promise { if (visible && !(await configuration.getToken())) { // Track if a user is not authenticated and expanded the analysis view @@ -93,10 +141,19 @@ export default class SnykLib extends BaseSnykModule { } } + private isSnykCodeAutoscanSuspended(manual: boolean) { + return !manual && !this.scanModeService.isCodeAutoScanAllowed(); + } + private logFullAnalysisIsTriggered(manual: boolean) { const analysisType: SupportedAnalysisProperties[] = []; const enabledFeatures = configuration.getFeaturesConfiguration(); + // Ensure preconditions are the same as within running specific analysis + if (!this.isSnykCodeAutoscanSuspended(manual)) { + if (enabledFeatures?.codeSecurityEnabled) analysisType.push('Snyk Code Security'); + if (enabledFeatures?.codeQualityEnabled) analysisType.push('Snyk Code Quality'); + } if (enabledFeatures?.ossEnabled) analysisType.push('Snyk Open Source'); if (analysisType.length) { diff --git a/src/snyk/base/services/authenticationService.ts b/src/snyk/base/services/authenticationService.ts index 7a9364a56..1f7f91f21 100644 --- a/src/snyk/base/services/authenticationService.ts +++ b/src/snyk/base/services/authenticationService.ts @@ -1,11 +1,10 @@ import { validate as uuidValidate } from 'uuid'; import { IAnalytics } from '../../common/analytics/itly'; import { IConfiguration } from '../../common/configuration/configuration'; -import { DID_CHANGE_CONFIGURATION_METHOD, SNYK_WORKSPACE_SCAN_COMMAND } from '../../common/constants/languageServer'; +import { DID_CHANGE_CONFIGURATION_METHOD } from '../../common/constants/languageServer'; import { SNYK_CONTEXT } from '../../common/constants/views'; import { ILog } from '../../common/logger/interfaces'; import { IContextService } from '../../common/services/contextService'; -import { IVSCodeCommands } from '../../common/vscode/commands'; import { ILanguageClientAdapter } from '../../common/vscode/languageClient'; import { IVSCodeWindow } from '../../common/vscode/window'; import { IBaseSnykModule } from '../modules/interfaces'; @@ -26,7 +25,6 @@ export class AuthenticationService implements IAuthenticationService { private readonly analytics: IAnalytics, private readonly logger: ILog, private readonly clientAdapter: ILanguageClientAdapter, - private commands: IVSCodeCommands, ) {} async initiateLogin(): Promise { @@ -68,8 +66,6 @@ export class AuthenticationService implements IAuthenticationService { await this.contextService.setContext(SNYK_CONTEXT.LOGGEDIN, true); this.baseModule.loadingBadge.setLoadingBadge(false); - - await this.commands.executeCommand(SNYK_WORKSPACE_SCAN_COMMAND); } } } diff --git a/src/snyk/base/services/scanModeService.ts b/src/snyk/base/services/scanModeService.ts new file mode 100644 index 000000000..3c1cc3136 --- /dev/null +++ b/src/snyk/base/services/scanModeService.ts @@ -0,0 +1,19 @@ +import { IAnalytics } from '../../common/analytics/itly'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { IContextService } from '../../common/services/contextService'; +import { CodeScanMode } from '../../snykCode/constants/modes'; + +export class ScanModeService { + private _mode = CodeScanMode.AUTO; + private _lastThrottledExecution: number | undefined; + + constructor(private contextService: IContextService, private config: IConfiguration, private analytics: IAnalytics) {} + + isOssAutoScanAllowed(): boolean { + return this.config.shouldAutoScanOss; + } + + isCodeAutoScanAllowed(): boolean { + return true; + } +} diff --git a/src/snyk/common/commands/commandController.ts b/src/snyk/common/commands/commandController.ts index 155149b83..c9a448173 100644 --- a/src/snyk/common/commands/commandController.ts +++ b/src/snyk/common/commands/commandController.ts @@ -1,16 +1,19 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import _ from 'lodash'; import { IAuthenticationService } from '../../base/services/authenticationService'; +import { ScanModeService } from '../../base/services/scanModeService'; import { ISnykCodeService } from '../../snykCode/codeService'; +import { ISnykCodeServiceOld } from '../../snykCode/codeServiceOld'; import { createDCIgnore } from '../../snykCode/utils/ignoreFileUtils'; import { IssueUtils } from '../../snykCode/utils/issueUtils'; -import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; +import { CodeIssueCommandArg, CodeIssueCommandArgOld } from '../../snykCode/views/interfaces'; import { capitalizeOssSeverity } from '../../snykOss/ossResult'; import { OssService } from '../../snykOss/services/ossService'; import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; import { IAnalytics } from '../analytics/itly'; import { SNYK_INITIATE_LOGIN_COMMAND, + SNYK_OPEN_BROWSER_COMMAND, SNYK_SET_TOKEN_COMMAND, VSCODE_GO_TO_SETTINGS_COMMAND, } from '../constants/commands'; @@ -19,6 +22,7 @@ import { SNYK_LOGIN_COMMAND, SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND } from '../con import { ErrorHandler } from '../error/errorHandler'; import { ILanguageServer } from '../languageServer/languageServer'; import { ILog } from '../logger/interfaces'; +import { IOpenerService } from '../services/openerService'; import { IVSCodeCommands } from '../vscode/commands'; import { Range, Uri } from '../vscode/types'; import { IUriAdapter } from '../vscode/uri'; @@ -30,9 +34,12 @@ export class CommandController { private debouncedCommands: Record Promise>> = {}; constructor( + private openerService: IOpenerService, private authService: IAuthenticationService, private snykCode: ISnykCodeService, + private snykCodeOld: ISnykCodeServiceOld, private ossService: OssService, + private scanModeService: ScanModeService, private workspace: IVSCodeWorkspace, private commands: IVSCodeCommands, private window: IVSCodeWindow, @@ -41,6 +48,10 @@ export class CommandController { private analytics: IAnalytics, ) {} + openBrowser(url: string): unknown { + return this.executeCommand(SNYK_OPEN_BROWSER_COMMAND, this.openerService.openBrowserUrl.bind(this), url); + } + async initiateLogin(): Promise { this.logger.info('Initiating login'); await this.executeCommand(SNYK_INITIATE_LOGIN_COMMAND, this.authService.initiateLogin.bind(this.authService)); @@ -108,6 +119,25 @@ export class CommandController { issueType: IssueUtils.getIssueType(issue.additionalData.isSecurityType), severity: IssueUtils.issueSeverityAsText(issue.severity), }); + } else if (arg.issueType == OpenCommandIssueType.CodeIssueOld) { + const issue = arg.issue as CodeIssueCommandArgOld; + const suggestion = this.snykCodeOld.analyzer.findSuggestion(issue.diagnostic); + if (!suggestion) return; + // Set openUri = null to avoid opening the file (e.g. in the ActionProvider) + await this.openLocal(issue.filePath, issue.range); + + try { + this.snykCodeOld.suggestionProvider.show(suggestion.id, issue.filePath, issue.range); + } catch (e) { + ErrorHandler.handle(e, this.logger); + } + + this.analytics.logIssueInTreeIsClicked({ + ide: IDE_NAME, + issueId: decodeURIComponent(suggestion.id), + issueType: IssueUtils.getIssueType(suggestion.isSecurityType), + severity: IssueUtils.severityAsText(suggestion.severity), + }); } else if (arg.issueType == OpenCommandIssueType.OssVulnerability) { const issue = arg.issue as OssIssueCommandArg; void this.ossService.showSuggestionProvider(issue); diff --git a/src/snyk/common/commands/types.ts b/src/snyk/common/commands/types.ts index 443839bb7..38a13d8be 100644 --- a/src/snyk/common/commands/types.ts +++ b/src/snyk/common/commands/types.ts @@ -1,4 +1,5 @@ -import { CodeIssueCommandArg } from '../../snykCode/views/interfaces'; +import { completeFileSuggestionType } from '../../snykCode/interfaces'; +import { CodeIssueCommandArg, CodeIssueCommandArgOld } from '../../snykCode/views/interfaces'; import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; import { CodeIssueData, Issue } from '../languageServer/types'; @@ -9,19 +10,30 @@ export enum OpenCommandIssueType { } export type OpenIssueCommandArg = { - issue: CodeIssueCommandArg | OssIssueCommandArg; + issue: CodeIssueCommandArg | CodeIssueCommandArgOld | OssIssueCommandArg; issueType: OpenCommandIssueType; }; +export type ReportFalsePositiveCommandArg = { + suggestion: Readonly; +}; + +export const isCodeIssueOld = ( + _issue: completeFileSuggestionType | Issue | OssIssueCommandArg, + issueType: OpenCommandIssueType, +): _issue is completeFileSuggestionType => { + return issueType === OpenCommandIssueType.CodeIssueOld; +}; + export const isCodeIssue = ( - _issue: Issue | OssIssueCommandArg, + _issue: completeFileSuggestionType | Issue | OssIssueCommandArg, issueType: OpenCommandIssueType, ): _issue is Issue => { return issueType === OpenCommandIssueType.CodeIssue; }; export const isOssIssue = ( - _issue: Issue | OssIssueCommandArg, + _issue: completeFileSuggestionType | Issue | OssIssueCommandArg, issueType: OpenCommandIssueType, ): _issue is OssIssueCommandArg => { return issueType === OpenCommandIssueType.OssVulnerability; diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 584da6fb4..3bc61873c 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -45,6 +45,8 @@ export interface SeverityFilter { } export type PreviewFeatures = { + lsCode: boolean | undefined; + reportFalsePositives: boolean | undefined; advisor: boolean | undefined; }; @@ -414,6 +416,8 @@ export class Configuration implements IConfiguration { getPreviewFeatures(): PreviewFeatures { const defaultSetting: PreviewFeatures = { + lsCode: false, + reportFalsePositives: false, advisor: false, }; diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts index accc2e25b..a48a80b57 100644 --- a/src/snyk/common/constants/commands.ts +++ b/src/snyk/common/constants/commands.ts @@ -11,7 +11,7 @@ export const SNYK_SET_TOKEN_COMMAND = 'snyk.setToken'; export const SNYK_ENABLE_CODE_COMMAND = 'snyk.enableCode'; export const SNYK_SETTINGS_COMMAND = 'snyk.settings'; export const SNYK_DCIGNORE_COMMAND = 'snyk.dcignore'; -export const SNYK_OPEN_BROWSER_COMMAND = 'snyk.openBrowser'; +export const SNYK_OPEN_BROWSER_COMMAND = 'snyk.open'; export const SNYK_OPEN_LOCAL_COMMAND = 'snyk.show'; export const SNYK_OPEN_ISSUE_COMMAND = 'snyk.showissue'; export const SNYK_IGNORE_ISSUE_COMMAND = 'snyk.ignoreissue'; diff --git a/src/snyk/common/constants/views.ts b/src/snyk/common/constants/views.ts index 1d750512a..9ffdade76 100644 --- a/src/snyk/common/constants/views.ts +++ b/src/snyk/common/constants/views.ts @@ -1,5 +1,8 @@ export const SNYK_VIEW_WELCOME = 'snyk.views.welcome'; export const SNYK_VIEW_FEATURES = 'snyk.views.features'; +export const SNYK_VIEW_ANALYSIS_CODE_ENABLEMENT = 'snyk.views.analysis.code.enablement'; +export const SNYK_VIEW_ANALYSIS_CODE_SECURITY_OLD = 'snyk.views.analysis.code.security.old'; +export const SNYK_VIEW_ANALYSIS_CODE_QUALITY_OLD = 'snyk.views.analysis.code.quality.old'; export const SNYK_VIEW_ANALYSIS_CODE_SECURITY = 'snyk.views.analysis.code.security'; export const SNYK_VIEW_ANALYSIS_CODE_QUALITY = 'snyk.views.analysis.code.quality'; export const SNYK_VIEW_ANALYSIS_OSS = 'snyk.views.analysis.oss'; @@ -15,6 +18,9 @@ export const SNYK_CONTEXT = { LOGGEDIN: 'loggedIn', AUTHENTICATING: 'authenticating', FEATURES_SELECTED: 'featuresSelected', + CODE_ENABLED: 'codeEnabled', + CODE_LOCAL_ENGINE_ENABLED: 'codeLocalEngineEnabled', + LS_CODE_PREVIEW: 'lsCodePreview', WORKSPACE_FOUND: 'workspaceFound', ERROR: 'error', MODE: 'mode', @@ -25,7 +31,11 @@ export const SNYK_ERROR_CODES = { BLOCKING: 'blocking', }; -export const SNYK_SCAN_STATUS = { +export const SNYK_ANALYSIS_STATUS = { + FILTERS: 'Supported extentions', + COLLECTING: 'Collecting files', + BUNDLING: 'Creating file bundles', + UPLOADING: 'Uploading files', OSS_DISABLED: 'Snyk Open Source Security is disabled. Enable it in settings to use it.', CODE_SECURITY_DISABLED: 'Snyk Code Security is disabled. Enable it in settings to use it.', CODE_QUALITY_DISABLED: 'Snyk Code Quality is disabled. Enable it in settings to use it.', diff --git a/src/snyk/common/languageServer/settings.ts b/src/snyk/common/languageServer/settings.ts index 2879a23e1..351f73b57 100644 --- a/src/snyk/common/languageServer/settings.ts +++ b/src/snyk/common/languageServer/settings.ts @@ -34,12 +34,12 @@ export class LanguageServerSettings { const featuresConfiguration = configuration.getFeaturesConfiguration(); const iacEnabled = _.isUndefined(featuresConfiguration.iacEnabled) ? true : featuresConfiguration.iacEnabled; - const codeSecurityEnabled = featuresConfiguration?.codeSecurityEnabled; - const codeQualityEnabled = featuresConfiguration?.codeQualityEnabled; + const codeSecurityEnabled = configuration.getPreviewFeatures().lsCode && featuresConfiguration?.codeSecurityEnabled; + const codeQualityEnabled = configuration.getPreviewFeatures().lsCode && featuresConfiguration?.codeQualityEnabled; return { - activateSnykCodeSecurity: `${codeSecurityEnabled}`, - activateSnykCodeQuality: `${codeQualityEnabled}`, + activateSnykCodeSecurity: `${codeSecurityEnabled ?? false}`, + activateSnykCodeQuality: `${codeQualityEnabled ?? false}`, activateSnykOpenSource: 'false', activateSnykIac: `${iacEnabled}`, enableTelemetry: `${configuration.shouldReportEvents}`, diff --git a/src/snyk/common/services/cliConfigService.ts b/src/snyk/common/services/cliConfigService.ts new file mode 100644 index 000000000..8f2f9b600 --- /dev/null +++ b/src/snyk/common/services/cliConfigService.ts @@ -0,0 +1,25 @@ +import { ISnykApiClient } from '../api/apiСlient'; +import { Configuration, IConfiguration } from '../configuration/configuration'; + +export type SastSettings = { + sastEnabled: boolean; + localCodeEngine: { + enabled: boolean; + }; + reportFalsePositivesEnabled: boolean; +}; + +export async function getSastSettings(api: ISnykApiClient, config: IConfiguration): Promise { + const response = await api.get('cli-config/settings/sast', { + headers: { + 'x-snyk-ide': `${Configuration.source}-${await Configuration.getVersion()}`, + }, + params: { + ...(config.organization ? { org: config.organization } : {}), + }, + }); + + if (!response) return; + + return response.data; +} diff --git a/src/snyk/common/services/contextService.ts b/src/snyk/common/services/contextService.ts index 86e85ebc4..5227d10a7 100644 --- a/src/snyk/common/services/contextService.ts +++ b/src/snyk/common/services/contextService.ts @@ -4,6 +4,7 @@ import { setContext } from '../vscode/vscodeCommandsUtils'; export interface IContextService { readonly viewContext: { [key: string]: unknown }; + shouldShowCodeAnalysis: boolean; shouldShowOssAnalysis: boolean; setContext(key: string, value: unknown): Promise; @@ -22,6 +23,10 @@ export class ContextService implements IContextService { await setContext(key, value); } + get shouldShowCodeAnalysis(): boolean { + return this.shouldShowAnalysis && !!this.viewContext[SNYK_CONTEXT.CODE_ENABLED]; + } + get shouldShowOssAnalysis(): boolean { return this.shouldShowAnalysis; } diff --git a/src/snyk/common/services/learnService.ts b/src/snyk/common/services/learnService.ts index 3c9fd69ff..740ba2ee8 100644 --- a/src/snyk/common/services/learnService.ts +++ b/src/snyk/common/services/learnService.ts @@ -1,6 +1,7 @@ import axios from 'axios'; -import { isCodeIssue, isOssIssue, OpenCommandIssueType } from '../../common/commands/types'; +import { isCodeIssue, isCodeIssueOld, isOssIssue, OpenCommandIssueType } from '../../common/commands/types'; import { SNYK_LEARN_API_CACHE_DURATION_IN_MS } from '../../common/constants/general'; +import type { completeFileSuggestionType } from '../../snykCode/interfaces'; import { OssIssueCommandArg } from '../../snykOss/views/ossVulnerabilityTreeProvider'; import { IConfiguration } from '../configuration/configuration'; import { ErrorHandler } from '../error/errorHandler'; @@ -36,6 +37,17 @@ export class LearnService { private readonly shouldCacheRequests = true, ) {} + // TODO: remove when Code results come from LS + static getCodeIssueParamsOld(issue: completeFileSuggestionType): LessonLookupParams { + const idParts = issue.id.split(/\/|%2F/g); + + return { + rule: idParts[idParts.length - 1], + ecosystem: idParts[0], + cwes: issue.cwe, + }; + } + static getCodeIssueParams(issue: Issue): LessonLookupParams { const idParts = issue.additionalData.ruleId.split(/\/|%2F/g); @@ -84,13 +96,17 @@ export class LearnService { } async getLesson( - issue: OssIssueCommandArg | Issue, + issue: OssIssueCommandArg | completeFileSuggestionType | Issue, issueType: OpenCommandIssueType, ): Promise { try { let params: LessonLookupParams | null = null; - if (isCodeIssue(issue, issueType)) { + if (isCodeIssueOld(issue, issueType)) { + if (!issue.isSecurityType) return null; + + params = LearnService.getCodeIssueParamsOld(issue); + } else if (isCodeIssue(issue, issueType)) { if (!issue.additionalData.isSecurityType) return null; params = LearnService.getCodeIssueParams(issue); diff --git a/src/snyk/common/services/openerService.ts b/src/snyk/common/services/openerService.ts new file mode 100644 index 000000000..14a4f00dd --- /dev/null +++ b/src/snyk/common/services/openerService.ts @@ -0,0 +1,18 @@ +import open from 'open'; +import { ErrorHandler } from '../error/errorHandler'; +import { Logger } from '../logger/logger'; + +export interface IOpenerService { + openBrowserUrl(url: string): Promise; +} + +// TODO: use Language Server to open browser urls +export class OpenerService { + async openBrowserUrl(url: string): Promise { + try { + await open(url); + } catch (err) { + ErrorHandler.handle(err, Logger); + } + } +} diff --git a/src/snyk/common/views/analysisTreeNodeProvider.ts b/src/snyk/common/views/analysisTreeNodeProvider.ts index 988e87f3c..0a3098dca 100644 --- a/src/snyk/common/views/analysisTreeNodeProvider.ts +++ b/src/snyk/common/views/analysisTreeNodeProvider.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import * as path from 'path'; import { AnalysisStatusProvider } from '../analysis/statusProvider'; import { IConfiguration } from '../configuration/configuration'; -import { SNYK_SHOW_LS_OUTPUT_COMMAND } from '../constants/commands'; +import { SNYK_SHOW_LS_OUTPUT_COMMAND, SNYK_SHOW_OUTPUT_COMMAND } from '../constants/commands'; import { messages } from '../messages/analysisMessages'; import { NODE_ICONS, TreeNode } from './treeNode'; import { TreeNodeProvider } from './treeNodeProvider'; @@ -58,6 +58,8 @@ export abstract class AnalysisTreeNodeProvder extends TreeNodeProvider { } protected getErrorEncounteredTreeNode(scanPath?: string): TreeNode { + const lsCodeEnabled = this.configuration.getPreviewFeatures()?.lsCode; + return new TreeNode({ icon: NODE_ICONS.error, text: scanPath ? path.basename(scanPath) : messages.scanFailed, @@ -66,17 +68,19 @@ export abstract class AnalysisTreeNodeProvder extends TreeNodeProvider { isError: true, }, command: { - command: SNYK_SHOW_LS_OUTPUT_COMMAND, + command: lsCodeEnabled ? SNYK_SHOW_LS_OUTPUT_COMMAND : SNYK_SHOW_OUTPUT_COMMAND, title: '', }, }); } protected getNoWorkspaceTrustTreeNode(): TreeNode { + const lsCodeEnabled = this.configuration.getPreviewFeatures()?.lsCode; + return new TreeNode({ text: messages.noWorkspaceTrust, command: { - command: SNYK_SHOW_LS_OUTPUT_COMMAND, + command: lsCodeEnabled ? SNYK_SHOW_LS_OUTPUT_COMMAND : SNYK_SHOW_OUTPUT_COMMAND, title: '', }, }); diff --git a/src/snyk/common/watchers/configurationWatcher.ts b/src/snyk/common/watchers/configurationWatcher.ts index 4b1c63031..85e6890b2 100644 --- a/src/snyk/common/watchers/configurationWatcher.ts +++ b/src/snyk/common/watchers/configurationWatcher.ts @@ -26,11 +26,18 @@ class ConfigurationWatcher implements IWatcher { constructor(private readonly analytics: IAnalytics, private readonly logger: ILog) {} private async onChangeConfiguration(extension: IExtension, key: string): Promise { - if (key === YES_TELEMETRY_SETTING) { + if (key === ADVANCED_ADVANCED_MODE_SETTING) { + return extension.checkAdvancedMode(); + } else if (key === YES_TELEMETRY_SETTING) { return this.analytics.setShouldReportEvents(configuration.shouldReportEvents); } else if (key === OSS_ENABLED_SETTING) { extension.viewManagerService.refreshOssView(); + } else if (key === CODE_SECURITY_ENABLED_SETTING || key === CODE_QUALITY_ENABLED_SETTING) { + extension.snykCodeOld.analyzer.refreshDiagnostics(); + // If two settings are changed simultaneously, only one will be applied, thus refresh all views + return extension.viewManagerService.refreshAllOldCodeAnalysisViews(); } else if (key === SEVERITY_FILTER_SETTING) { + extension.snykCodeOld.analyzer.refreshDiagnostics(); return extension.viewManagerService.refreshAllViews(); } else if (key === ADVANCED_CUSTOM_ENDPOINT) { return configuration.clearToken(); diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index bbaeebb2f..3cce129ac 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -4,6 +4,7 @@ import { AdvisorService } from './advisor/services/advisorService'; import { IExtension } from './base/modules/interfaces'; import SnykLib from './base/modules/snykLib'; import { AuthenticationService } from './base/services/authenticationService'; +import { ScanModeService } from './base/services/scanModeService'; import { EmptyTreeDataProvider } from './base/views/emptyTreeDataProvider'; import { FeaturesViewProvider } from './base/views/featureSelection/featuresViewProvider'; import { SupportProvider } from './base/views/supportProvider'; @@ -15,8 +16,10 @@ import { configuration } from './common/configuration/instance'; import { SnykConfiguration } from './common/configuration/snykConfiguration'; import { SNYK_DCIGNORE_COMMAND, + SNYK_ENABLE_CODE_COMMAND, SNYK_IGNORE_ISSUE_COMMAND, SNYK_INITIATE_LOGIN_COMMAND, + SNYK_OPEN_BROWSER_COMMAND, SNYK_OPEN_ISSUE_COMMAND, SNYK_OPEN_LOCAL_COMMAND, SNYK_SETTINGS_COMMAND, @@ -28,8 +31,12 @@ import { import { MEMENTO_FIRST_INSTALL_DATE_KEY } from './common/constants/globalState'; import { SNYK_WORKSPACE_SCAN_COMMAND } from './common/constants/languageServer'; import { + SNYK_CONTEXT, + SNYK_VIEW_ANALYSIS_CODE_ENABLEMENT, SNYK_VIEW_ANALYSIS_CODE_QUALITY, + SNYK_VIEW_ANALYSIS_CODE_QUALITY_OLD, SNYK_VIEW_ANALYSIS_CODE_SECURITY, + SNYK_VIEW_ANALYSIS_CODE_SECURITY_OLD, SNYK_VIEW_ANALYSIS_OSS, SNYK_VIEW_FEATURES, SNYK_VIEW_SUPPORT, @@ -50,7 +57,7 @@ import { vsCodeEnv } from './common/vscode/env'; import { extensionContext } from './common/vscode/extensionContext'; import { HoverAdapter } from './common/vscode/hover'; import { LanguageClientAdapter } from './common/vscode/languageClient'; -import { vsCodeLanguages } from './common/vscode/languages'; +import { vsCodeLanguages, VSCodeLanguages } from './common/vscode/languages'; import SecretStorageAdapter from './common/vscode/secretStorage'; import { ThemeColorAdapter } from './common/vscode/theme'; import { Range, Uri } from './common/vscode/types'; @@ -60,8 +67,11 @@ import { vsCodeWorkspace } from './common/vscode/workspace'; import ConfigurationWatcher from './common/watchers/configurationWatcher'; import { IgnoreCommand } from './snykCode/codeActions/ignoreCommand'; import { SnykCodeService } from './snykCode/codeService'; +import { SnykCodeServiceOld } from './snykCode/codeServiceOld'; import { CodeQualityIssueTreeProvider } from './snykCode/views/qualityIssueTreeProvider'; +import { CodeQualityIssueTreeProviderOld } from './snykCode/views/qualityIssueTreeProviderOld'; import CodeSecurityIssueTreeProvider from './snykCode/views/securityIssueTreeProvider'; +import { CodeSecurityIssueTreeProviderOld } from './snykCode/views/securityIssueTreeProviderOld'; import { CodeSuggestionWebviewProvider } from './snykCode/views/suggestion/codeSuggestionWebviewProvider'; import { NpmTestApi } from './snykOss/api/npmTestApi'; import { EditorDecorator } from './snykOss/editor/editorDecorator'; @@ -131,9 +141,28 @@ class SnykExtension extends SnykLib implements IExtension { this.analytics, Logger, languageClientAdapter, - vsCodeComands, ); + this.snykCodeOld = new SnykCodeServiceOld( + this.context, + configuration, + this.viewManagerService, + vsCodeWorkspace, + vsCodeWindow, + this.user, + this.falsePositiveApi, + Logger, + this.analytics, + new VSCodeLanguages(), + this.snykCodeErrorHandler, + new UriAdapter(), + this.codeSettings, + this.learnService, + this.markdownStringAdapter, + this.workspaceTrust, + ); + this.scanModeService = new ScanModeService(this.contextService, configuration, this.analytics); + this.advisorService = new AdvisorProvider(this.advisorApiClient, Logger); this.downloadService = new DownloadService( this.context, @@ -154,30 +183,34 @@ class SnykExtension extends SnykLib implements IExtension { this.downloadService, ); - const codeSuggestionProvider = new CodeSuggestionWebviewProvider( - vsCodeWindow, - extensionContext, - Logger, - vsCodeLanguages, - vsCodeWorkspace, - this.learnService, - ); + const lsCodePreview = configuration.getPreviewFeatures().lsCode; + if (lsCodePreview) { + await this.contextService.setContext(SNYK_CONTEXT.LS_CODE_PREVIEW, true); + const codeSuggestionProvider = new CodeSuggestionWebviewProvider( + vsCodeWindow, + extensionContext, + Logger, + vsCodeLanguages, + vsCodeWorkspace, + this.learnService, + ); - this.snykCode = new SnykCodeService( - this.context, - configuration, - codeSuggestionProvider, - new CodeActionAdapter(), - this.codeActionKindAdapter, - this.viewManagerService, - vsCodeWorkspace, - this.workspaceTrust, - this.languageServer, - vsCodeWindow, - vsCodeLanguages, - Logger, - this.analytics, - ); + this.snykCode = new SnykCodeService( + this.context, + configuration, + codeSuggestionProvider, + new CodeActionAdapter(), + this.codeActionKindAdapter, + this.viewManagerService, + vsCodeWorkspace, + this.workspaceTrust, + this.languageServer, + vsCodeWindow, + vsCodeLanguages, + Logger, + this.analytics, + ); + } this.ossService = new OssService( this.context, @@ -195,9 +228,12 @@ class SnykExtension extends SnykLib implements IExtension { ); this.commandController = new CommandController( + this.openerService, this.authService, this.snykCode, + this.snykCodeOld, this.ossService, + this.scanModeService, vsCodeWorkspace, vsCodeComands, vsCodeWindow, @@ -207,34 +243,49 @@ class SnykExtension extends SnykLib implements IExtension { ); this.registerCommands(vscodeContext); - const codeSecurityIssueProvider = new CodeSecurityIssueTreeProvider( + const codeSecurityIssueProvider = new CodeSecurityIssueTreeProviderOld( this.viewManagerService, this.contextService, - this.snykCode, + this.snykCodeOld, configuration, - vsCodeLanguages, ), - codeQualityIssueProvider = new CodeQualityIssueTreeProvider( + codeQualityIssueProvider = new CodeQualityIssueTreeProviderOld( this.viewManagerService, this.contextService, - this.snykCode, + this.snykCodeOld, configuration, - vsCodeLanguages, ); - const codeSecurityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_SECURITY, { - treeDataProvider: codeSecurityIssueProvider, - }); - const codeQualityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_QUALITY, { - treeDataProvider: codeQualityIssueProvider, - }); + if (lsCodePreview) { + const codeSecurityIssueProvider = new CodeSecurityIssueTreeProvider( + this.viewManagerService, + this.contextService, + this.snykCode, + configuration, + vsCodeLanguages, + ), + codeQualityIssueProvider = new CodeQualityIssueTreeProvider( + this.viewManagerService, + this.contextService, + this.snykCode, + configuration, + vsCodeLanguages, + ); + + const codeSecurityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_SECURITY, { + treeDataProvider: codeSecurityIssueProvider, + }); + const codeQualityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_QUALITY, { + treeDataProvider: codeQualityIssueProvider, + }); - vscodeContext.subscriptions.push( - vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_SECURITY, codeSecurityIssueProvider), - vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_QUALITY, codeQualityIssueProvider), - codeSecurityTree, - codeQualityTree, - ); + vscodeContext.subscriptions.push( + vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_SECURITY, codeSecurityIssueProvider), + vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_QUALITY, codeQualityIssueProvider), + codeSecurityTree, + codeQualityTree, + ); + } const ossVulnerabilityProvider = new OssVulnerabilityTreeProvider( this.viewManagerService, @@ -248,18 +299,33 @@ class SnykExtension extends SnykLib implements IExtension { vscodeContext.subscriptions.push( vscode.window.registerWebviewViewProvider(SNYK_VIEW_FEATURES, featuresViewProvider), vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_OSS, ossVulnerabilityProvider), + vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_SECURITY_OLD, codeSecurityIssueProvider), + vscode.window.registerTreeDataProvider(SNYK_VIEW_ANALYSIS_CODE_QUALITY_OLD, codeQualityIssueProvider), vscode.window.registerTreeDataProvider(SNYK_VIEW_SUPPORT, new SupportProvider()), ); const welcomeTree = vscode.window.createTreeView(SNYK_VIEW_WELCOME, { treeDataProvider: new EmptyTreeDataProvider(), }); + const codeEnablementTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_ENABLEMENT, { + treeDataProvider: new EmptyTreeDataProvider(), + }); + const ossTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_OSS, { treeDataProvider: ossVulnerabilityProvider, }); + const codeSecurityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_SECURITY_OLD, { + treeDataProvider: codeSecurityIssueProvider, + }); + const codeQualityTree = vscode.window.createTreeView(SNYK_VIEW_ANALYSIS_CODE_QUALITY_OLD, { + treeDataProvider: codeQualityIssueProvider, + }); vscodeContext.subscriptions.push( ossTree.onDidChangeVisibility(e => this.onDidChangeOssTreeVisibility(e.visible)), + codeSecurityTree, + codeQualityTree, welcomeTree.onDidChangeVisibility(e => this.onDidChangeWelcomeViewVisibility(e.visible)), + codeEnablementTree, ); // Fill the view container to expose views for tests @@ -275,8 +341,12 @@ class SnykExtension extends SnykLib implements IExtension { this.runScan(false); }); + this.editorsWatcher.activate(this); this.configurationWatcher.activate(this); - this.snykCode.activateWebviewProviders(); + if (lsCodePreview) { + this.snykCode.activateWebviewProviders(); + } + this.snykCodeOld.activateWebviewProviders(); this.ossService.activateSuggestionProvider(); this.ossService.activateManifestFileWatcher(this); @@ -335,6 +405,7 @@ class SnykExtension extends SnykLib implements IExtension { } public async deactivate(): Promise { + this.snykCodeOld.dispose(); this.ossVulnerabilityCountService.dispose(); await this.languageServer.stop(); await this.analytics.flush(); @@ -366,11 +437,17 @@ class SnykExtension extends SnykLib implements IExtension { private registerCommands(context: vscode.ExtensionContext): void { context.subscriptions.push( + vscode.commands.registerCommand(SNYK_OPEN_BROWSER_COMMAND, (url: string) => + this.commandController.openBrowser(url), + ), vscode.commands.registerCommand(SNYK_OPEN_LOCAL_COMMAND, (path: Uri, range?: Range | undefined) => this.commandController.openLocal(path, range), ), vscode.commands.registerCommand(SNYK_INITIATE_LOGIN_COMMAND, () => this.commandController.initiateLogin()), vscode.commands.registerCommand(SNYK_SET_TOKEN_COMMAND, () => this.commandController.setToken()), + vscode.commands.registerCommand(SNYK_ENABLE_CODE_COMMAND, () => + this.commandController.executeCommand(SNYK_ENABLE_CODE_COMMAND, () => this.enableCode()), + ), vscode.commands.registerCommand(SNYK_START_COMMAND, async () => { await vscode.commands.executeCommand(SNYK_WORKSPACE_SCAN_COMMAND); // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/src/snyk/snykCode/analyzer/analyzer.ts b/src/snyk/snykCode/analyzer/analyzer.ts new file mode 100644 index 000000000..9973f2b71 --- /dev/null +++ b/src/snyk/snykCode/analyzer/analyzer.ts @@ -0,0 +1,286 @@ +import { AnalysisResultLegacy, AnalysisSeverity, FilePath, FileSuggestion } from '@snyk/code-client'; +import { IExtension } from '../../base/modules/interfaces'; +import { IAnalytics } from '../../common/analytics/itly'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { ILog } from '../../common/logger/interfaces'; +import { errorsLogs } from '../../common/messages/errors'; +import { IHoverAdapter } from '../../common/vscode/hover'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { IMarkdownStringAdapter } from '../../common/vscode/markdownString'; +import { + Diagnostic, + DiagnosticCollection, + DiagnosticSeverity, + Disposable, + Range, + Uri, +} from '../../common/vscode/types'; +import { IUriAdapter } from '../../common/vscode/uri'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; +import { + DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME, + DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME, +} from '../constants/analysis'; +import { ISnykCodeErrorHandler } from '../error/snykCodeErrorHandler'; +import { DisposableHoverProvider } from '../hoverProvider/hoverProvider'; +import { + completeFileSuggestionType, + ICodeSuggestion, + IIssuesListOptions, + ISnykCodeAnalyzer, + ISnykCodeResult, + openedTextEditorType, +} from '../interfaces'; +import { + checkCompleteSuggestion, + createIssueCorrectRange, + createIssueRelatedInformation, + createSnykSeveritiesMap, + findCompleteSuggestion, + isSecurityTypeSuggestion, + updateFileReviewResultsPositions, +} from '../utils/analysisUtils'; + +class SnykCodeAnalyzer implements ISnykCodeAnalyzer { + protected disposables: Disposable[] = []; + + private SEVERITIES: { + [key: number]: { name: DiagnosticSeverity }; + }; + public readonly codeQualityReview: DiagnosticCollection | undefined; + public readonly codeSecurityReview: DiagnosticCollection | undefined; + private analysisResults: ISnykCodeResult; + + private readonly diagnosticSuggestion = new Map(); + + public constructor( + private readonly logger: ILog, + private readonly languages: IVSCodeLanguages, + private readonly workspace: IVSCodeWorkspace, + private readonly analytics: IAnalytics, + private readonly errorHandler: ISnykCodeErrorHandler, + private readonly uriAdapter: IUriAdapter, + private readonly configuration: IConfiguration, + ) { + this.SEVERITIES = createSnykSeveritiesMap(); + this.codeSecurityReview = this.languages.createDiagnosticCollection(DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME); + this.codeQualityReview = this.languages.createDiagnosticCollection(DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME); + + this.disposables.push(this.codeSecurityReview, this.codeQualityReview); + } + + public registerCodeActionProviders( + codeSecurityCodeActionsProvider: Disposable, + codeQualityCodeActionsProvider: Disposable, + ) { + this.disposables.push(codeSecurityCodeActionsProvider, codeQualityCodeActionsProvider); + } + + public registerHoverProviders( + codeSecurityHoverAdapter: IHoverAdapter, + codeQualityHoverAdapter: IHoverAdapter, + markdownStringAdapter: IMarkdownStringAdapter, + ): void { + this.disposables.push( + new DisposableHoverProvider(this, this.logger, this.languages, this.analytics, markdownStringAdapter).register( + this.codeSecurityReview, + codeSecurityHoverAdapter, + ), + new DisposableHoverProvider(this, this.logger, this.languages, this.analytics, markdownStringAdapter).register( + this.codeQualityReview, + codeQualityHoverAdapter, + ), + ); + } + + public setAnalysisResults(results: AnalysisResultLegacy): void { + Object.values(results.suggestions).forEach(suggestion => { + suggestion['isSecurityType'] = isSecurityTypeSuggestion(suggestion); + }); + + this.analysisResults = results as ISnykCodeResult; + } + + public getAnalysisResults(): ISnykCodeResult { + return this.analysisResults; + } + + dispose(): void { + this.diagnosticSuggestion.clear(); + + while (this.disposables.length) { + const disposable = this.disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } + + public getFullSuggestion(suggestionId: string, uri: Uri, position: Range): completeFileSuggestionType | undefined { + return findCompleteSuggestion(this.analysisResults, suggestionId, uri, position, this.languages); + } + + public checkFullSuggestion(suggestion: completeFileSuggestionType): boolean { + return checkCompleteSuggestion(this.analysisResults, suggestion, this.uriAdapter); + } + + public findSuggestion(diagnostic: Diagnostic): ICodeSuggestion | undefined { + return this.diagnosticSuggestion.get(diagnostic); + } + + private createIssueDiagnosticInfo({ + issuePositions, + suggestion, + fileUri, + }: { + issuePositions: FileSuggestion; + suggestion: ICodeSuggestion; + fileUri: Uri; + }): Diagnostic { + const { message } = suggestion; + return { + code: '', + message, + range: createIssueCorrectRange(issuePositions, this.languages), + severity: this.SEVERITIES[suggestion.severity].name, + source: suggestion.isSecurityType + ? DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME + : DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME, + // issues markers can be in issuesPositions as prop 'markers', + ...(issuePositions.markers && { + relatedInformation: createIssueRelatedInformation( + issuePositions.markers, + fileUri.path, + message, + this.languages, + this.workspace, + this.uriAdapter, + ), + }), + }; + } + + private createDiagnostics(options: IIssuesListOptions): [securityIssues: Diagnostic[], qualityIssues: Diagnostic[]] { + const securityIssues: Diagnostic[] = []; + const qualityIssues: Diagnostic[] = []; + + const { fileIssuesList, suggestions, fileUri } = options; + + for (const issue in fileIssuesList) { + const isSecurityType = suggestions[issue].isSecurityType; + + if (!SnykCodeAnalyzer.isIssueVisible(this.configuration, isSecurityType, suggestions[issue].severity)) { + continue; + } + + const issueList = isSecurityType ? securityIssues : qualityIssues; + for (const issuePositions of fileIssuesList[issue]) { + const suggestion = suggestions[issue]; + const diagnostic = this.createIssueDiagnosticInfo({ + issuePositions, + suggestion, + fileUri, + }); + + this.diagnosticSuggestion.set(diagnostic, suggestion); + issueList.push(diagnostic); + } + } + + return [securityIssues, qualityIssues]; + } + + public createReviewResults(): void { + if (!this.codeSecurityReview || !this.codeQualityReview) { + return; + } + this.codeSecurityReview.clear(); + this.codeQualityReview.clear(); + this.diagnosticSuggestion.clear(); + + if (!this.analysisResults) { + return; + } + + const { files, suggestions } = this.analysisResults; + for (const filePath in files) { + if (!files.hasOwnProperty(filePath)) { + continue; + } + + const fileUri = this.uriAdapter.file(filePath); + if (!fileUri) { + return; + } + const fileIssuesList = files[filePath]; + const [securityIssues, qualityIssues] = this.createDiagnostics({ + fileIssuesList, + suggestions, + fileUri, + }); + + if (securityIssues.length > 0) this.codeSecurityReview.set(fileUri, [...securityIssues]); + if (qualityIssues.length > 0) this.codeQualityReview.set(fileUri, [...qualityIssues]); + } + } + + public async updateReviewResultsPositions(extension: IExtension, updatedFile: openedTextEditorType): Promise { + try { + const isSecurityReviewFile = this.codeSecurityReview && this.codeSecurityReview.has(updatedFile.document.uri); + const isQualityReviewFile = this.codeQualityReview && this.codeQualityReview.has(updatedFile.document.uri); + + if ( + (!isSecurityReviewFile && !isQualityReviewFile) || + !updatedFile.contentChanges.length || + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + !updatedFile.contentChanges[0].range + ) { + return; + } + const fileIssuesList: FilePath = updateFileReviewResultsPositions(this.analysisResults, updatedFile); + const [securityIssues, qualityIssues] = this.createDiagnostics({ + fileIssuesList, + suggestions: this.analysisResults.suggestions, + fileUri: this.uriAdapter.file(updatedFile.fullPath), + }); + if (isSecurityReviewFile) { + this.codeSecurityReview?.set(this.uriAdapter.file(updatedFile.fullPath), [...securityIssues]); + } else if (isQualityReviewFile) { + this.codeQualityReview?.set(this.uriAdapter.file(updatedFile.fullPath), [...qualityIssues]); + } + } catch (err) { + await this.errorHandler.processError(err, { + message: errorsLogs.updateReviewPositions, + bundleId: extension.snykCodeOld.remoteBundle?.fileBundle.bundleHash, + data: { + [updatedFile.fullPath]: updatedFile.contentChanges, + }, + }); + } + } + + static isIssueVisible(configuration: IConfiguration, isSecurityType: boolean, severity: AnalysisSeverity): boolean { + if (isSecurityType && !configuration.getFeaturesConfiguration()?.codeSecurityEnabled) { + return false; + // deepcode ignore DuplicateIfBody: readability + } else if (!isSecurityType && !configuration.getFeaturesConfiguration()?.codeQualityEnabled) { + return false; + } + + switch (severity) { + case AnalysisSeverity.critical: + return configuration.severityFilter.high; + case AnalysisSeverity.warning: + return configuration.severityFilter.medium; + case AnalysisSeverity.info: + return configuration.severityFilter.low; + default: + return false; + } + } + + // Refreshes reported diagnostic + public refreshDiagnostics = () => this.createReviewResults(); +} + +export default SnykCodeAnalyzer; diff --git a/src/snyk/snykCode/analyzer/progress.ts b/src/snyk/snykCode/analyzer/progress.ts new file mode 100644 index 000000000..2f9728a9d --- /dev/null +++ b/src/snyk/snykCode/analyzer/progress.ts @@ -0,0 +1,79 @@ +import { emitter as emitterDC, SupportedFiles } from '@snyk/code-client'; +import _ from 'lodash'; +import * as vscode from 'vscode'; +import { getExtension } from '../../../extension'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; +import { Logger } from '../../common/logger/logger'; +import { IViewManagerService } from '../../common/services/viewManagerService'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; +import { ISnykCodeServiceOld } from '../codeServiceOld'; +import createFileWatcher from '../watchers/filesWatcher'; + +export class Progress { + private emitter = emitterDC; + private filesWatcher: vscode.FileSystemWatcher; + + constructor( + private readonly snykCode: ISnykCodeServiceOld, + private readonly viewManagerService: IViewManagerService, + private readonly workspace: IVSCodeWorkspace, + ) {} + + bindListeners(): void { + this.emitter.on(this.emitter.events.supportedFilesLoaded, (data: SupportedFiles | null) => + this.onSupportedFilesLoaded(data), + ); + this.emitter.on(this.emitter.events.scanFilesProgress, (value: number) => this.onScanFilesProgress(value)); + this.emitter.on(this.emitter.events.createBundleProgress, (processed: number, total: number) => + this.onCreateBundleProgress(processed, total), + ); + this.emitter.on(this.emitter.events.uploadBundleProgress, (processed: number, total: number) => + this.onUploadBundleProgress(processed, total), + ); + this.emitter.on(this.emitter.events.analyseProgress, (data: { status: string; progress: number }) => + this.onAnalyseProgress(data), + ); + this.emitter.on(this.emitter.events.apiRequestLog, (message: string) => Progress.onAPIRequestLog(message)); + this.emitter.on(this.emitter.events.error, (requestId: string) => this.snykCode.errorEncountered(requestId)); + } + + updateStatus(status: string, progress: string): void { + this.snykCode.updateStatus(status, progress); + this.viewManagerService.refreshAllOldCodeAnalysisViews(); + } + + onSupportedFilesLoaded(data: SupportedFiles | null): void { + const msg = data ? 'Ignore rules loading' : 'Loading'; + + this.updateStatus(SNYK_ANALYSIS_STATUS.FILTERS, msg); + + // Setup file watcher + if (!this.filesWatcher && data) { + this.filesWatcher = createFileWatcher(getExtension(), this.workspace, data); + } + } + + onScanFilesProgress(value: number): void { + this.updateStatus(SNYK_ANALYSIS_STATUS.COLLECTING, `${value}`); + } + + onCreateBundleProgress(processed: number, total: number): void { + this.updateStatus(SNYK_ANALYSIS_STATUS.BUNDLING, `${processed}/${total}`); + } + + onUploadBundleProgress(processed: number, total: number): void { + this.updateStatus(SNYK_ANALYSIS_STATUS.UPLOADING, `${processed}/${total}`); + } + + onAnalyseProgress(data: { status: string; progress: number }): void { + this.updateStatus(_.capitalize(_.toLower(data.status)), `${Math.round(100 * data.progress)}%`); + } + + removeAllListeners(): void { + this.emitter.removeAllListeners(); + } + + static onAPIRequestLog(message: string): void { + Logger.debug(message.slice(0, 399)); + } +} diff --git a/src/snyk/snykCode/codeActions/disposableCodeActionsProvider.ts b/src/snyk/snykCode/codeActions/disposableCodeActionsProvider.ts new file mode 100644 index 000000000..892f4b9ea --- /dev/null +++ b/src/snyk/snykCode/codeActions/disposableCodeActionsProvider.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import * as vscode from 'vscode'; +import { IAnalytics } from '../../common/analytics/itly'; +import { CodeActionAdapter, CodeActionKindAdapter } from '../../common/vscode/codeAction'; +import { Disposable } from '../../common/vscode/types'; +import { SnykIssuesActionProviderOld } from './issuesActionsProviderOld'; + +export type CodeActionsCallbackFunctions = { [key: string]: (x: unknown) => any }; + +export class DisposableCodeActionsProvider implements Disposable { + private disposableProvider: vscode.Disposable | undefined; + constructor( + snykReview: vscode.DiagnosticCollection | undefined, + callbacks: CodeActionsCallbackFunctions, + readonly analytics: IAnalytics, + ) { + this.registerProvider(snykReview, callbacks); + } + + private registerProvider( + snykReview: vscode.DiagnosticCollection | undefined, + callbacks: CodeActionsCallbackFunctions, + ) { + const provider = new SnykIssuesActionProviderOld( + snykReview, + callbacks, + new CodeActionAdapter(), + new CodeActionKindAdapter(), + this.analytics, + ); + this.disposableProvider = vscode.languages.registerCodeActionsProvider( + { scheme: 'file', language: '*' }, + provider, + { + providedCodeActionKinds: provider.getProvidedCodeActionKinds(), + }, + ); + } + + dispose(): void { + if (this.disposableProvider) { + this.disposableProvider.dispose(); + } + } +} diff --git a/src/snyk/snykCode/codeActions/issuesActionsProviderOld.ts b/src/snyk/snykCode/codeActions/issuesActionsProviderOld.ts new file mode 100644 index 000000000..8e2dc0283 --- /dev/null +++ b/src/snyk/snykCode/codeActions/issuesActionsProviderOld.ts @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/ban-types */ +import { IAnalytics } from '../../common/analytics/itly'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; +import { SNYK_IGNORE_ISSUE_COMMAND, SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; +import { IDE_NAME } from '../../common/constants/general'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../common/vscode/codeAction'; +import { + CodeAction, + CodeActionKind, + CodeActionProvider, + Diagnostic, + DiagnosticCollection, + Range, + TextDocument, +} from '../../common/vscode/types'; +import { FILE_IGNORE_ACTION_NAME, IGNORE_ISSUE_ACTION_NAME, SHOW_ISSUE_ACTION_NAME } from '../constants/analysis'; +import { ICodeSuggestion } from '../interfaces'; +import { IssueUtils } from '../utils/issueUtils'; +import { CodeIssueCommandArgOld } from '../views/interfaces'; +import { CodeActionsCallbackFunctions } from './disposableCodeActionsProvider'; + +export class SnykIssuesActionProviderOld implements CodeActionProvider { + private readonly providedCodeActionKinds = [this.codeActionKindProvider.getQuickFix()]; + + private issuesList: DiagnosticCollection | undefined; + private findSuggestion: (diagnostic: Diagnostic) => ICodeSuggestion | undefined; + private trackIgnoreSuggestion: (vscodeSeverity: number, options: { [key: string]: any }) => void; + + constructor( + issuesList: DiagnosticCollection | undefined, + callbacks: CodeActionsCallbackFunctions, + private readonly codeActionAdapter: ICodeActionAdapter, + private readonly codeActionKindProvider: ICodeActionKindAdapter, + private readonly analytics: IAnalytics, + ) { + this.issuesList = issuesList; + this.findSuggestion = callbacks.findSuggestion; + this.trackIgnoreSuggestion = callbacks.trackIgnoreSuggestion; + } + + getProvidedCodeActionKinds(): CodeActionKind[] { + return this.providedCodeActionKinds; + } + + private createIgnoreIssueAction({ + document, + matchedIssue, + isFileIgnore, + }: { + document: TextDocument; + matchedIssue: Diagnostic; + isFileIgnore?: boolean; + }): CodeAction { + const ignoreIssueAction = this.codeActionAdapter.create( + isFileIgnore ? FILE_IGNORE_ACTION_NAME : IGNORE_ISSUE_ACTION_NAME, + this.providedCodeActionKinds[0], + ); + + const suggestion = this.findSuggestion(matchedIssue); + if (suggestion) + ignoreIssueAction.command = { + command: SNYK_IGNORE_ISSUE_COMMAND, + title: SNYK_IGNORE_ISSUE_COMMAND, + arguments: [{ uri: document.uri, matchedIssue, ruleId: suggestion.rule, isFileIgnore }], + }; + + return ignoreIssueAction; + } + + private createShowIssueAction({ + document, + matchedIssue, + }: { + document: TextDocument; + matchedIssue: Diagnostic; + }): CodeAction { + const showIssueAction = this.codeActionAdapter.create(SHOW_ISSUE_ACTION_NAME, this.providedCodeActionKinds[0]); + + const suggestion = this.findSuggestion(matchedIssue); + if (suggestion) + showIssueAction.command = { + command: SNYK_OPEN_ISSUE_COMMAND, + title: SNYK_OPEN_ISSUE_COMMAND, + arguments: [ + { + issueType: OpenCommandIssueType.CodeIssueOld, + issue: { + message: matchedIssue.message, + filePath: document.uri, + range: matchedIssue.range, + diagnostic: matchedIssue, + } as CodeIssueCommandArgOld, + } as OpenIssueCommandArg, + ], + }; + + return showIssueAction; + } + + public provideCodeActions(document: TextDocument, clickedRange: Range): CodeAction[] | undefined { + if (!this.issuesList || !this.issuesList.has(document.uri)) { + return undefined; + } + const fileIssues = this.issuesList && this.issuesList.get(document.uri); + const matchedIssue = IssueUtils.findIssueWithRange(clickedRange, fileIssues); + if (matchedIssue) { + const codeActionParams = { document, matchedIssue }; + const showIssueAction = this.createShowIssueAction(codeActionParams); + const ignoreIssueAction = this.createIgnoreIssueAction(codeActionParams); + const fileIgnoreIssueAction = this.createIgnoreIssueAction({ + ...codeActionParams, + isFileIgnore: true, + }); + + this.analytics.logQuickFixIsDisplayed({ + quickFixType: ['Show Suggestion', 'Ignore Suggestion In Line', 'Ignore Suggestion In File'], + ide: IDE_NAME, + }); + + // returns list of actions, all new actions should be added to this list + return [showIssueAction, ignoreIssueAction, fileIgnoreIssueAction]; + } + + return undefined; + } +} diff --git a/src/snyk/snykCode/codeServiceOld.ts b/src/snyk/snykCode/codeServiceOld.ts new file mode 100644 index 000000000..fa6d7d200 --- /dev/null +++ b/src/snyk/snykCode/codeServiceOld.ts @@ -0,0 +1,361 @@ +import { AnalysisSeverity, analyzeFolders, extendAnalysis, FileAnalysis } from '@snyk/code-client'; +import { ConnectionOptions } from '@snyk/code-client/dist/http'; +import { v4 as uuidv4 } from 'uuid'; +import { AnalysisStatusProvider } from '../common/analysis/statusProvider'; +import { IAnalytics, SupportedAnalysisProperties } from '../common/analytics/itly'; +import { FeaturesConfiguration, IConfiguration } from '../common/configuration/configuration'; +import { IWorkspaceTrust } from '../common/configuration/trustedFolders'; +import { IDE_NAME } from '../common/constants/general'; +import { ErrorHandler } from '../common/error/errorHandler'; +import { ILog } from '../common/logger/interfaces'; +import { Logger } from '../common/logger/logger'; +import { messages as generalAnalysisMessages } from '../common/messages/analysisMessages'; +import { LearnService } from '../common/services/learnService'; +import { IViewManagerService } from '../common/services/viewManagerService'; +import { User } from '../common/user'; +import { IWebViewProvider } from '../common/views/webviewProvider'; +import { ExtensionContext } from '../common/vscode/extensionContext'; +import { HoverAdapter } from '../common/vscode/hover'; +import { IVSCodeLanguages } from '../common/vscode/languages'; +import { IMarkdownStringAdapter } from '../common/vscode/markdownString'; +import { Diagnostic, Disposable } from '../common/vscode/types'; +import { IUriAdapter } from '../common/vscode/uri'; +import { IVSCodeWindow } from '../common/vscode/window'; +import { IVSCodeWorkspace } from '../common/vscode/workspace'; +import SnykCodeAnalyzer from './analyzer/analyzer'; +import { Progress } from './analyzer/progress'; +import { DisposableCodeActionsProvider } from './codeActions/disposableCodeActionsProvider'; +import { ICodeSettings } from './codeSettings'; +import { ISnykCodeErrorHandler } from './error/snykCodeErrorHandler'; +import { IFalsePositiveApi } from './falsePositive/api/falsePositiveApi'; +import { FalsePositive } from './falsePositive/falsePositive'; +import { ISnykCodeAnalyzer } from './interfaces'; +import { messages as analysisMessages } from './messages/analysis'; +import { messages } from './messages/error'; +import { IssueUtils } from './utils/issueUtils'; +import { + FalsePositiveWebviewModel, + FalsePositiveWebviewProvider, +} from './views/falsePositive/falsePositiveWebviewProvider'; +import { ICodeSuggestionWebviewProviderOld } from './views/interfaces'; +import { CodeSuggestionWebviewProviderOld } from './views/suggestion/codeSuggestionWebviewProviderOld'; + +export interface ISnykCodeServiceOld extends AnalysisStatusProvider, Disposable { + analyzer: ISnykCodeAnalyzer; + analysisStatus: string; + analysisProgress: string; + readonly remoteBundle: FileAnalysis | null; + readonly suggestionProvider: ICodeSuggestionWebviewProviderOld; + hasError: boolean; + hasTransientError: boolean; + isAnyWorkspaceFolderTrusted: boolean; + + startAnalysis(paths: string[], manual: boolean, reportTriggeredEvent: boolean): Promise; + clearBundle(): void; + updateStatus(status: string, progress: string): void; + errorEncountered(requestId: string): void; + addChangedFile(filePath: string): void; + activateWebviewProviders(): void; + reportFalsePositive( + falsePositive: FalsePositive, + isSecurityTypeIssue: boolean, + issueSeverity: AnalysisSeverity, + ): Promise; +} + +export class SnykCodeServiceOld extends AnalysisStatusProvider implements ISnykCodeServiceOld { + remoteBundle: FileAnalysis | null; + analyzer: ISnykCodeAnalyzer; + readonly suggestionProvider: ICodeSuggestionWebviewProviderOld; + readonly falsePositiveProvider: IWebViewProvider; + + private changedFiles: Set = new Set(); + + private progress: Progress; + private _analysisStatus = ''; + private _analysisProgress = ''; + private temporaryFailed = false; + private failed = false; + private _isAnyWorkspaceFolderTrusted = true; + + constructor( + readonly extensionContext: ExtensionContext, + private readonly config: IConfiguration, + private readonly viewManagerService: IViewManagerService, + private readonly workspace: IVSCodeWorkspace, + readonly window: IVSCodeWindow, + private readonly user: User, + private readonly falsePositiveApi: IFalsePositiveApi, + private readonly logger: ILog, + private readonly analytics: IAnalytics, + readonly languages: IVSCodeLanguages, + private readonly errorHandler: ISnykCodeErrorHandler, + private readonly uriAdapter: IUriAdapter, + codeSettings: ICodeSettings, + private readonly learnService: LearnService, + private readonly markdownStringAdapter: IMarkdownStringAdapter, + private readonly workspaceTrust: IWorkspaceTrust, + ) { + super(); + this.analyzer = new SnykCodeAnalyzer( + logger, + languages, + workspace, + analytics, + errorHandler, + this.uriAdapter, + this.config, + ); + this.registerAnalyzerProviders(this.analyzer); + + this.falsePositiveProvider = new FalsePositiveWebviewProvider( + this, + this.window, + extensionContext, + this.logger, + this.analytics, + ); + this.suggestionProvider = new CodeSuggestionWebviewProviderOld( + config, + this.analyzer, + window, + extensionContext, + this.logger, + languages, + workspace, + codeSettings, + this.learnService, + ); + + this.progress = new Progress(this, viewManagerService, this.workspace); + this.progress.bindListeners(); + } + + get hasError(): boolean { + return this.failed; + } + get hasTransientError(): boolean { + return this.temporaryFailed; + } + get isAnyWorkspaceFolderTrusted(): boolean { + return this._isAnyWorkspaceFolderTrusted; + } + + get analysisStatus(): string { + return this._analysisStatus; + } + get analysisProgress(): string { + return this._analysisProgress; + } + + public async startAnalysis(paths: string[], manualTrigger: boolean, reportTriggeredEvent: boolean): Promise { + if (this.isAnalysisRunning || !paths.length) { + return; + } + + const enabledFeatures = this.config.getFeaturesConfiguration(); + const requestId = uuidv4(); + + const pathsToTest = this.workspaceTrust.getTrustedFolders(this.config, paths); + if (!pathsToTest.length) { + this._isAnyWorkspaceFolderTrusted = false; + this.viewManagerService.refreshCodeAnalysisViews(enabledFeatures); + this.logger.info(`Skipping Code scan. ${generalAnalysisMessages.noWorkspaceTrustDescription}`); + return; + } + this._isAnyWorkspaceFolderTrusted = true; + + try { + Logger.info(analysisMessages.started); + + // reset error state + this.temporaryFailed = false; + this.failed = false; + + this.reportAnalysisIsTriggered(reportTriggeredEvent, enabledFeatures, manualTrigger); + this.analysisStarted(); + + const snykCodeToken = await this.config.snykCodeToken; + + let result: FileAnalysis | null = null; + if (this.changedFiles.size && this.remoteBundle) { + const changedFiles = [...this.changedFiles]; + result = await extendAnalysis({ + ...this.remoteBundle, + files: changedFiles, + connection: this.getConnectionOptions(requestId, snykCodeToken), + }); + } else { + result = await analyzeFolders({ + connection: this.getConnectionOptions(requestId, snykCodeToken), + analysisOptions: { + legacy: true, + }, + fileOptions: { + paths: pathsToTest.concat([]), + }, + analysisContext: { + flow: this.config.source, + initiator: 'IDE', + orgDisplayName: this.config.organization, + }, + }); + } + + if (result) { + this.remoteBundle = result; + + if (result.analysisResults.type == 'legacy') { + this.analyzer.setAnalysisResults(result.analysisResults); + } + this.analyzer.createReviewResults(); + + Logger.info(analysisMessages.finished); + + if (enabledFeatures?.codeSecurityEnabled) { + this.analytics.logAnalysisIsReady({ + ide: IDE_NAME, + analysisType: 'Snyk Code Security', + result: 'Success', + }); + } + if (enabledFeatures?.codeQualityEnabled) { + this.analytics.logAnalysisIsReady({ + ide: IDE_NAME, + analysisType: 'Snyk Code Quality', + result: 'Success', + }); + } + + this.suggestionProvider.checkCurrentSuggestion(); + + // cleanup analysis state + this.changedFiles.clear(); + } + } catch (err) { + this.temporaryFailed = true; + await this.errorHandler.processError(err, undefined, requestId, () => { + this.errorEncountered(requestId); + }); + + if (enabledFeatures?.codeSecurityEnabled && this.errorHandler.connectionRetryLimitExhausted) { + this.analytics.logAnalysisIsReady({ + ide: IDE_NAME, + analysisType: 'Snyk Code Security', + result: 'Error', + }); + } + if (enabledFeatures?.codeQualityEnabled && this.errorHandler.connectionRetryLimitExhausted) { + this.analytics.logAnalysisIsReady({ + ide: IDE_NAME, + analysisType: 'Snyk Code Quality', + result: 'Error', + }); + } + } finally { + this.analysisFinished(); + this.viewManagerService.refreshCodeAnalysisViews(enabledFeatures); + } + } + + clearBundle() { + this.remoteBundle = null; + } + + private reportAnalysisIsTriggered( + reportTriggeredEvent: boolean, + enabledFeatures: FeaturesConfiguration | undefined, + manualTrigger: boolean, + ) { + if (reportTriggeredEvent) { + const analysisType: SupportedAnalysisProperties[] = []; + if (enabledFeatures?.codeSecurityEnabled) analysisType.push('Snyk Code Security'); + if (enabledFeatures?.codeQualityEnabled) analysisType.push('Snyk Code Quality'); + + if (analysisType) { + this.analytics.logAnalysisIsTriggered({ + analysisType: analysisType as [SupportedAnalysisProperties, ...SupportedAnalysisProperties[]], + ide: IDE_NAME, + triggeredByUser: manualTrigger, + }); + } + } + } + + updateStatus(status: string, progress: string): void { + this._analysisStatus = status; + this._analysisProgress = progress; + } + + errorEncountered(requestId: string): void { + this.temporaryFailed = false; + this.failed = true; + this.logger.error(analysisMessages.failed(requestId)); + } + + addChangedFile(filePath: string): void { + this.changedFiles.add(filePath); + } + + activateWebviewProviders(): void { + this.suggestionProvider.activate(); + this.falsePositiveProvider.activate(); + } + + async reportFalsePositive( + falsePositive: FalsePositive, + isSecurityTypeIssue: boolean, + issueSeverity: AnalysisSeverity, + ): Promise { + try { + await this.falsePositiveApi.report(falsePositive, this.user); + + this.analytics.logFalsePositiveIsSubmitted({ + issueId: falsePositive.id, + issueType: IssueUtils.getIssueType(isSecurityTypeIssue), + severity: IssueUtils.severityAsText(issueSeverity), + }); + } catch (e) { + ErrorHandler.handle(e, this.logger, messages.reportFalsePositiveFailed); + } + } + + dispose(): void { + this.progress.removeAllListeners(); + this.analyzer.dispose(); + } + + private getConnectionOptions(requestId: string, snykCodeToken: string | undefined): ConnectionOptions { + if (!snykCodeToken) { + throw new Error('Snyk token must be filled to obtain connection options'); + } + + return { + baseURL: this.config.snykCodeBaseURL, + sessionToken: snykCodeToken, + source: this.config.source, + requestId, + base64Encoding: true, + }; + } + + private registerAnalyzerProviders(analyzer: ISnykCodeAnalyzer) { + analyzer.registerHoverProviders(new HoverAdapter(), new HoverAdapter(), this.markdownStringAdapter); + analyzer.registerCodeActionProviders( + new DisposableCodeActionsProvider( + analyzer.codeSecurityReview, + { + findSuggestion: (diagnostic: Diagnostic) => analyzer.findSuggestion(diagnostic), + }, + this.analytics, + ), + new DisposableCodeActionsProvider( + analyzer.codeQualityReview, + { + findSuggestion: (diagnostic: Diagnostic) => analyzer.findSuggestion(diagnostic), + }, + this.analytics, + ), + ); + } +} diff --git a/src/snyk/snykCode/codeSettings.ts b/src/snyk/snykCode/codeSettings.ts new file mode 100644 index 000000000..08064bd1b --- /dev/null +++ b/src/snyk/snykCode/codeSettings.ts @@ -0,0 +1,81 @@ +import { ISnykApiClient } from '../common/api/apiСlient'; +import { IConfiguration } from '../common/configuration/configuration'; +import { SNYK_CONTEXT } from '../common/constants/views'; +import { getSastSettings, SastSettings } from '../common/services/cliConfigService'; +import { IContextService } from '../common/services/contextService'; +import { IOpenerService } from '../common/services/openerService'; + +export interface ICodeSettings { + reportFalsePositivesEnabled: boolean; + + checkCodeEnabled(): Promise; + enable(): Promise; + getSastSettings(): Promise; +} + +export class CodeSettings implements ICodeSettings { + private _reportFalsePositivesEnabled: boolean; + + get reportFalsePositivesEnabled(): boolean { + return this._reportFalsePositivesEnabled; + } + + constructor( + private readonly snykApiClient: ISnykApiClient, + private readonly contextService: IContextService, + private readonly config: IConfiguration, + private readonly openerService: IOpenerService, + ) {} + + async checkCodeEnabled(): Promise { + const settings = await this.getSastSettings(); + if (!settings) { + return false; + } + + await this.contextService.setContext(SNYK_CONTEXT.CODE_ENABLED, settings.sastEnabled); + await this.contextService.setContext( + SNYK_CONTEXT.CODE_LOCAL_ENGINE_ENABLED, + settings.localCodeEngine.enabled ?? false, + ); + + return settings.sastEnabled && !settings.localCodeEngine.enabled; + } + + async enable(): Promise { + let settings = await this.getSastSettings(); + if (settings?.sastEnabled) { + return true; + } + + if (this.config.snykCodeUrl != null) { + await this.openerService.openBrowserUrl(this.config.snykCodeUrl); + } + + // Poll for changed settings (65 sec) + for (let i = 2; i < 12; i += 1) { + // eslint-disable-next-line no-await-in-loop + await this.sleep(i * 1000); + + // eslint-disable-next-line no-await-in-loop + settings = await this.getSastSettings(); + if (settings?.sastEnabled) { + return true; + } + } + + return false; + } + + async getSastSettings(): Promise { + const settings = await getSastSettings(this.snykApiClient, this.config); + if (settings) { + // cache if false positive reports are enabled. + this._reportFalsePositivesEnabled = settings.reportFalsePositivesEnabled; + } + + return settings; + } + + private sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration)); +} diff --git a/src/snyk/snykCode/constants/analysis.ts b/src/snyk/snykCode/constants/analysis.ts index ad2f36036..a9dc2baf4 100644 --- a/src/snyk/snykCode/constants/analysis.ts +++ b/src/snyk/snykCode/constants/analysis.ts @@ -1,3 +1,9 @@ +export const SNYK_SEVERITIES: { [key: string]: number } = { + information: 1, + warning: 2, + error: 3, +}; + export const IGNORE_ISSUE_BASE_COMMENT_TEXT = 'deepcode ignore'; export const FILE_IGNORE_ISSUE_BASE_COMMENT_TEXT = `file ${IGNORE_ISSUE_BASE_COMMENT_TEXT}`; @@ -9,6 +15,14 @@ export const IGNORE_ISSUE_ACTION_NAME = 'Ignore this particular suggestion (Snyk export const FILE_IGNORE_ACTION_NAME = 'Ignore this suggestion in current file (Snyk)'; export const IGNORE_TIP_FOR_USER = "To ignore this issue for Snyk choose 'Ignore this issue' in QuickFix dropdown"; +export const ISSUES_MARKERS_DECORATION_TYPE: { [key: string]: string } = { + border: '1px', + borderColor: 'green', + borderStyle: 'none none dashed none', +}; + +export const DIAGNOSTICS_CODE_SECURITY_COLLECTION_NAME = 'Snyk Code Security'; +export const DIAGNOSTICS_CODE_QUALITY_COLLECTION_NAME = 'Snyk Code Quality'; export const DIAGNOSTICS_OSS_COLLECTION_NAME = 'Snyk Open Source Security'; export const WEBVIEW_PANEL_SECURITY_TITLE = 'Snyk Code Vulnerability'; diff --git a/src/snyk/snykCode/constants/modes.ts b/src/snyk/snykCode/constants/modes.ts new file mode 100644 index 000000000..0aeea7e61 --- /dev/null +++ b/src/snyk/snykCode/constants/modes.ts @@ -0,0 +1,6 @@ +export enum CodeScanMode { + AUTO = 'auto', + MANUAL = 'manual', + PAUSED = 'paused', + THROTTLED = 'throttled', +} diff --git a/src/snyk/snykCode/error/snykCodeErrorHandler.ts b/src/snyk/snykCode/error/snykCodeErrorHandler.ts new file mode 100644 index 000000000..ce9c91a88 --- /dev/null +++ b/src/snyk/snykCode/error/snykCodeErrorHandler.ts @@ -0,0 +1,199 @@ +import { constants } from '@snyk/code-client'; +import { errorType, IBaseSnykModule } from '../../base/modules/interfaces'; +import { ILoadingBadge } from '../../base/views/loadingBadge'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { CONNECTION_ERROR_RETRY_INTERVAL, MAX_CONNECTION_RETRIES } from '../../common/constants/general'; +import { SNYK_CONTEXT } from '../../common/constants/views'; +import { ErrorHandler } from '../../common/error/errorHandler'; +import { TagKeys, Tags } from '../../common/error/errorReporter'; +import { ILog } from '../../common/logger/interfaces'; +import { IContextService } from '../../common/services/contextService'; + +type SnykCodeErrorResponseType = { + apiName: string; + errorCode: string; + messages: { [key: number]: unknown }; +}; + +class SnykCodeErrorResponse { + constructor(public error: SnykCodeErrorResponseType) {} +} + +export interface ISnykCodeErrorHandler { + resetTransientErrors(): void; + get connectionRetryLimitExhausted(): boolean; + processError( + error: errorType, + options?: { [key: string]: unknown }, + requestId?: string, + callback?: (error: Error) => void, + ): Promise; +} + +export class SnykCodeErrorHandler extends ErrorHandler implements ISnykCodeErrorHandler { + private transientErrors = 0; + private _requestId: string | undefined; + private _connectionRetryLimitExhausted = false; + + constructor( + private contextService: IContextService, + private loadingBadge: ILoadingBadge, + private readonly logger: ILog, + private readonly baseSnykModule: IBaseSnykModule, + private readonly configuration: IConfiguration, + ) { + super(); + } + + resetTransientErrors(): void { + this.transientErrors = 0; + } + + resetRequestId(): void { + this._requestId = undefined; + } + + get connectionRetryLimitExhausted(): boolean { + return this._connectionRetryLimitExhausted; + } + + private isAuthenticationError(errorStatusCode: PropertyKey): boolean { + return errorStatusCode === constants.ErrorCodes.unauthorizedUser; + } + + private isBundleError(error: errorType): boolean { + // checkBundle API call returns 404 sometimes that gets propagated as an Error to us from 'code-client', treat as a transient error [ROAD-683] + return error instanceof Error && error.message === 'Failed to get remote bundle'; + } + + private async authenticationErrorHandler(): Promise { + await this.configuration.setToken(''); + await this.contextService.setContext(SNYK_CONTEXT.LOGGEDIN, false); + this.loadingBadge.setLoadingBadge(true); + } + + static isErrorRetryable(errorStatusCode: PropertyKey): boolean { + switch (errorStatusCode) { + case constants.ErrorCodes.badGateway: + case constants.ErrorCodes.serviceUnavailable: + case constants.ErrorCodes.serverError: + case constants.ErrorCodes.timeout: + case constants.ErrorCodes.dnsNotFound: + case constants.ErrorCodes.connectionRefused: + case constants.ErrorCodes.notFound: + return true; + + default: + return false; + } + } + + private extractErrorResponse(error: errorType) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!(error instanceof Error) && error?.apiName) { + // Error can come in different shapes, see https://github.com/snyk/code-client/blob/b5eb140e1400049caf8cbb133a951ab007b031d0/src/http.ts#L43. Extract all. + const { apiName, statusCode, statusText, errorCode, messages } = error as { [key: string]: string }; + if (errorCode) { + return new SnykCodeErrorResponse({ apiName, errorCode, messages }); + } + + return new SnykCodeErrorResponse({ apiName, errorCode: statusCode, messages: statusText }); + } + } + + async processError( + error: errorType, + options: { [key: string]: unknown } = {}, + requestId: string, + callback: (error: Error) => void, + ): Promise { + // We don't want to have unhandled rejections around, so if it + // happens in the error handler we just log it + + this._requestId = requestId; + const errorResponse = this.extractErrorResponse(error); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const updatedError = errorResponse ? errorResponse : error; + + return this.processErrorInternal(updatedError, options, callback).catch(err => + ErrorHandler.handle(err, this.logger, 'Snyk Code error handler failed with error.', { + [TagKeys.CodeRequestId]: this._requestId, + }), + ); + } + + private async processErrorInternal( + error: errorType, + options: { [key: string]: unknown } = {}, + callback: (error: Error) => void, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const errorStatusCode = (error?.statusCode as PropertyKey) || (error?.error?.errorCode as PropertyKey); + + if (this.isAuthenticationError(errorStatusCode)) { + return await this.authenticationErrorHandler(); + } + + if (SnykCodeErrorHandler.isErrorRetryable(errorStatusCode) || this.isBundleError(error)) { + return await this.retryHandler(error, errorStatusCode, options, callback); + } + + this._connectionRetryLimitExhausted = true; + this.generalErrorHandler(error, options, callback); + + return Promise.resolve(); + } + + private generalErrorHandler( + error: errorType, + options: { [key: string]: unknown }, + callback: (error: errorType) => void, + ): void { + this.transientErrors = 0; + callback(error); + + this.capture(error, options, { [TagKeys.CodeRequestId]: this._requestId }); + this.resetRequestId(); + } + + private async retryHandler( + error: errorType, + errorStatusCode: PropertyKey, + options: { [key: string]: unknown }, + callback: (error: Error) => void, + ): Promise { + this.logger.error(`Connection error to Snyk Code. Try count: ${this.transientErrors + 1}.`); + + if (this.transientErrors > MAX_CONNECTION_RETRIES) { + this._connectionRetryLimitExhausted = true; + this.generalErrorHandler(error, options, callback); + return; + } + + this.transientErrors += 1; + + if (errorStatusCode === constants.ErrorCodes.notFound) { + this.baseSnykModule.snykCodeOld.clearBundle(); // bundle has expired, trigger complete new analysis + } + + setTimeout(() => { + this.baseSnykModule.runCodeScan().catch(err => this.capture(err, options)); + }, CONNECTION_ERROR_RETRY_INTERVAL); + + return Promise.resolve(); + } + + capture(error: errorType, options: { [key: string]: unknown }, tags?: Tags): void { + if (error instanceof SnykCodeErrorResponse) { + error = new Error(JSON.stringify(error?.error)); + } + + let msg = error instanceof Error ? error?.message : ''; + if (Object.keys(options).length > 0) { + msg += `. ${JSON.stringify(options)}`; + } + + ErrorHandler.handle(error, this.logger, msg, tags); + } +} diff --git a/src/snyk/snykCode/falsePositive/api/falsePositiveApi.ts b/src/snyk/snykCode/falsePositive/api/falsePositiveApi.ts new file mode 100644 index 000000000..845f02824 --- /dev/null +++ b/src/snyk/snykCode/falsePositive/api/falsePositiveApi.ts @@ -0,0 +1,50 @@ +import { SnykApiClient } from '../../../common/api/apiСlient'; +import { User } from '../../../common/user'; +import { FalsePositive } from '../falsePositive'; + +export interface IFalsePositiveApi { + report(falsePositive: FalsePositive, user: User): Promise; +} + +type FalsePositivePayload = { + topic: string; + message: string; + feedbackOrigin: 'ide'; + context: { + issueId: string; + userPublicId: string; + startLine: number; + endLine: number; + primaryFilePath: string; + vulnName: string; + fileContents: string; + }; +}; + +export class FalsePositiveApi extends SnykApiClient implements IFalsePositiveApi { + report(falsePositive: FalsePositive, user: User): Promise { + if (!falsePositive.content?.length) { + throw new Error("False positive shouldn't be empty."); + } + if (!user.authenticatedId) { + throw new Error('Unauthenticated users cannot report false positives.'); + } + + const payload: FalsePositivePayload = { + topic: 'False Positive', + message: falsePositive.message, + feedbackOrigin: 'ide', + context: { + issueId: falsePositive.id, + fileContents: falsePositive.content, + startLine: falsePositive.startLine, + endLine: falsePositive.endLine, + primaryFilePath: falsePositive.primaryFilePath, + userPublicId: user.authenticatedId, + vulnName: falsePositive.rule, + }, + }; + + return this.post('feedback/sast', payload); + } +} diff --git a/src/snyk/snykCode/falsePositive/falsePositive.ts b/src/snyk/snykCode/falsePositive/falsePositive.ts new file mode 100644 index 000000000..2e74ee675 --- /dev/null +++ b/src/snyk/snykCode/falsePositive/falsePositive.ts @@ -0,0 +1,73 @@ +import { Marker } from '@snyk/code-client'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; +import { completeFileSuggestionType } from '../interfaces'; +import { getAbsoluteMarkerFilePath } from '../utils/analysisUtils'; +import { IssueUtils } from '../utils/issueUtils'; + +export class FalsePositive { + private files: Set; + + readonly message: string; + readonly id: string; + readonly startLine: number; + readonly endLine: number; + readonly primaryFilePath: string; + readonly rule: string; + content: string | undefined; + + constructor(private workspace: IVSCodeWorkspace, suggestion: completeFileSuggestionType) { + if (!suggestion.markers || suggestion.markers.length === 0) { + throw new Error('Cannot create false positive without markers.'); + } + + this.message = suggestion.message; + this.id = decodeURIComponent(suggestion.id); + this.rule = suggestion.rule; + this.primaryFilePath = suggestion.uri; + + const issuePosition = IssueUtils.createCorrectIssuePlacement(suggestion); + this.startLine = issuePosition.rows.start; + this.endLine = issuePosition.rows.end; + + this.files = this.getFiles(suggestion.markers, suggestion.uri); + } + + private getFiles(markers: Marker[], uri: string): Set { + const markerPositions = markers.flatMap(marker => marker.pos); + const filesArray = markerPositions.map(markerPosition => + getAbsoluteMarkerFilePath(this.workspace, markerPosition.file, uri), + ); + + return new Set(filesArray); + } + + /** + * May throw an error if file cannot be resolved. + */ + async getGeneratedContent(): Promise { + let content = this.getMainHeader(); + + for await (const file of this.files) { + const doc = await this.workspace.openFileTextDocument(file); + content += this.appendFileHeader(doc.getText(), file); + } + + return content; + } + + private getMainHeader() { + return `/** + * The following code will be uploaded to Snyk to be reviewed. + * Make sure there are no sensitive information sent. + */ + +`; + } + + private appendFileHeader(text: string, filePath: string) { + return `/** + * Code from ${filePath} + */ +${text}`; + } +} diff --git a/src/snyk/snykCode/hoverProvider/hoverProvider.ts b/src/snyk/snykCode/hoverProvider/hoverProvider.ts new file mode 100644 index 000000000..cafb460e9 --- /dev/null +++ b/src/snyk/snykCode/hoverProvider/hoverProvider.ts @@ -0,0 +1,68 @@ +import { IAnalytics } from '../../common/analytics/itly'; +import { IDE_NAME } from '../../common/constants/general'; +import { ILog } from '../../common/logger/interfaces'; +import { IHoverAdapter } from '../../common/vscode/hover'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { IMarkdownStringAdapter } from '../../common/vscode/markdownString'; +import { Diagnostic, DiagnosticCollection, Disposable, Hover, Position, TextDocument } from '../../common/vscode/types'; +import { IGNORE_TIP_FOR_USER } from '../constants/analysis'; +import { ISnykCodeAnalyzer } from '../interfaces'; +import { IssueUtils } from '../utils/issueUtils'; + +export class DisposableHoverProvider implements Disposable { + private hoverProvider: Disposable | undefined; + + constructor( + private readonly analyzer: ISnykCodeAnalyzer, + private readonly logger: ILog, + private readonly vscodeLanguages: IVSCodeLanguages, + private readonly analytics: IAnalytics, + private readonly markdownStringAdapter: IMarkdownStringAdapter, + ) {} + + register(snykReview: DiagnosticCollection | undefined, hoverAdapter: IHoverAdapter): Disposable { + this.hoverProvider = this.vscodeLanguages.registerHoverProvider( + { scheme: 'file', language: '*' }, + { + provideHover: this.getHover(snykReview, hoverAdapter), + }, + ); + return this; + } + + getHover(snykReview: DiagnosticCollection | undefined, hoverAdapter: IHoverAdapter) { + return (document: TextDocument, position: Position): Hover | undefined => { + if (!snykReview || !snykReview.has(document.uri)) { + return undefined; + } + const currentFileReviewIssues = snykReview.get(document.uri); + const issue = IssueUtils.findIssueWithRange(position, currentFileReviewIssues); + if (issue) { + this.logIssueHoverIsDisplayed(issue); + const ignoreMarkdown = this.markdownStringAdapter.get(IGNORE_TIP_FOR_USER); + return hoverAdapter.create(ignoreMarkdown); + } + }; + } + + private logIssueHoverIsDisplayed(issue: Diagnostic): void { + const suggestion = this.analyzer.findSuggestion(issue); + if (!suggestion) { + this.logger.debug('Failed to log hover displayed analytical event.'); + return; + } + + this.analytics.logIssueHoverIsDisplayed({ + issueId: suggestion.id, + issueType: IssueUtils.getIssueType(suggestion.isSecurityType), + severity: IssueUtils.severityAsText(suggestion.severity), + ide: IDE_NAME, + }); + } + + dispose(): void { + if (this.hoverProvider) { + this.hoverProvider.dispose(); + } + } +} diff --git a/src/snyk/snykCode/interfaces.ts b/src/snyk/snykCode/interfaces.ts new file mode 100644 index 000000000..482a99ca2 --- /dev/null +++ b/src/snyk/snykCode/interfaces.ts @@ -0,0 +1,70 @@ +import { AnalysisResultLegacy, FilePath, FileSuggestion, Suggestion } from '@snyk/code-client'; +import * as vscode from 'vscode'; +import { DiagnosticCollection, TextDocument } from 'vscode'; +import { IExtension } from '../base/modules/interfaces'; +import { IHoverAdapter } from '../common/vscode/hover'; +import { IMarkdownStringAdapter } from '../common/vscode/markdownString'; +import { Disposable } from '../common/vscode/types'; + +// TODO: remove after Code move to LS. +export type completeFileSuggestionType = ICodeSuggestion & + FileSuggestion & { + uri: string; + }; + +export type openedTextEditorType = { + fullPath: string; + workspace: string; + lineCount: { + current: number; + prevOffset: number; + }; + contentChanges: any[]; + document: TextDocument; +}; + +export interface IIssuesListOptions { + fileIssuesList: FilePath; + suggestions: Readonly; + fileUri: vscode.Uri; +} + +export type ICodeSuggestion = Suggestion & { + isSecurityType: boolean; +}; + +interface ICodeSuggestions { + [suggestionIndex: string]: Readonly; +} + +export interface ISnykCodeResult extends AnalysisResultLegacy { + suggestions: Readonly; +} + +export interface ISnykCodeAnalyzer extends Disposable { + codeSecurityReview: DiagnosticCollection | undefined; + codeQualityReview: DiagnosticCollection | undefined; + + registerHoverProviders( + codeSecurityHoverAdapter: IHoverAdapter, + codeQualityHoverAdapter: IHoverAdapter, + markdownStringAdapter: IMarkdownStringAdapter, + ): void; + registerCodeActionProviders( + codeSecurityCodeActionsProvider: Disposable, + codeQualityCodeActionsProvider: Disposable, + ): void; + + setAnalysisResults(results: AnalysisResultLegacy): void; + getAnalysisResults(): Readonly; + findSuggestion(diagnostic: vscode.Diagnostic): Readonly; + getFullSuggestion( + suggestionId: string, + uri: vscode.Uri, + position: vscode.Range, + ): Readonly; + checkFullSuggestion(suggestion: completeFileSuggestionType): boolean; + createReviewResults(): void; + updateReviewResultsPositions(extension: IExtension, updatedFile: openedTextEditorType): Promise; + refreshDiagnostics(): void; +} diff --git a/src/snyk/snykCode/messages/analysis.ts b/src/snyk/snykCode/messages/analysis.ts index 194b6bea6..2e9b2a4f1 100644 --- a/src/snyk/snykCode/messages/analysis.ts +++ b/src/snyk/snykCode/messages/analysis.ts @@ -1,3 +1,9 @@ export const messages = { runTest: 'Run scan for Code vulnerabilites and issues.', + started: 'Code analysis started.', + finished: 'Code analysis finished.', + temporaryFailed: 'Snyk Code is temporarily unavailable.', + retry: 'We are automatically retrying to connect...', + + failed: (requestId: string): string => `Code analysis failed. Request ID: ${requestId}`, }; diff --git a/src/snyk/snykCode/messages/error.ts b/src/snyk/snykCode/messages/error.ts index 814ad8470..3ccc43ba9 100644 --- a/src/snyk/snykCode/messages/error.ts +++ b/src/snyk/snykCode/messages/error.ts @@ -1,5 +1,8 @@ export const messages = { suggestionViewShowFailed: 'Failed to show Snyk Code suggestion view', + reportFalsePositiveViewShowFailed: 'Failed to show Snyk Code report false positive view', + reportFalsePositiveFailed: 'Failed to report false positive.', + suggestionViewMessageHandlingFailed: (msg: string): string => `Failed to handle message from Snyk Code suggestion view ${msg}`, }; diff --git a/src/snyk/snykCode/utils/analysisUtils.ts b/src/snyk/snykCode/utils/analysisUtils.ts index bcb99123c..f311ce96a 100644 --- a/src/snyk/snykCode/utils/analysisUtils.ts +++ b/src/snyk/snykCode/utils/analysisUtils.ts @@ -1,16 +1,298 @@ +/* eslint-disable @typescript-eslint/no-array-constructor */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { AnalysisResultLegacy, FilePath, FileSuggestion, Marker, Suggestion } from '@snyk/code-client'; import path from 'path'; +import { IVSCodeLanguages } from '../../common/vscode/languages'; +import { + DecorationOptions, + Diagnostic, + DiagnosticRelatedInformation, + DiagnosticSeverity, + Range, + Uri, +} from '../../common/vscode/types'; +import { IUriAdapter } from '../../common/vscode/uri'; import { IVSCodeWorkspace } from '../../common/vscode/workspace'; import { FILE_IGNORE_ISSUE_BASE_COMMENT_TEXT, IGNORE_ISSUE_BASE_COMMENT_TEXT, IGNORE_ISSUE_REASON_TIP, + SNYK_SEVERITIES, } from '../constants/analysis'; +import { completeFileSuggestionType, ICodeSuggestion, ISnykCodeResult, openedTextEditorType } from '../interfaces'; +import { IssuePlacementPosition, IssueUtils } from './issueUtils'; + +export const createSnykSeveritiesMap = (): { [x: number]: { name: DiagnosticSeverity } } => { + const { information, error, warning } = SNYK_SEVERITIES; + + return { + [information]: { + name: DiagnosticSeverity.Information, + }, + [warning]: { name: DiagnosticSeverity.Warning }, + [error]: { name: DiagnosticSeverity.Error }, + }; +}; + +export const getVSCodeSeverity = (snykSeverity: number): DiagnosticSeverity => { + const { information, error, warning } = SNYK_SEVERITIES; + return ( + { + [information]: DiagnosticSeverity.Information, + [warning]: DiagnosticSeverity.Warning, + [error]: DiagnosticSeverity.Error, + }[snykSeverity] || DiagnosticSeverity.Information + ); +}; + +export const getSnykSeverity = (vscodeSeverity: DiagnosticSeverity): number => { + const { information, error, warning } = SNYK_SEVERITIES; + return { + [DiagnosticSeverity.Information]: information, + [DiagnosticSeverity.Warning]: warning, + [DiagnosticSeverity.Error]: error, + [DiagnosticSeverity.Hint]: information, + }[vscodeSeverity]; +}; + +export const createSnykProgress = (progress: number): number => { + const progressOffset = 100; + return Math.round(progress * progressOffset); +}; + +export const createIssueRange = (position: IssuePlacementPosition, languages: IVSCodeLanguages): Range => { + return languages.createRange( + Math.max(0, position.rows.start), + Math.max(0, position.cols.start), + Math.max(0, position.rows.end), + Math.max(0, position.cols.end), + ); +}; + +// todo: remove when Snyk Code uses LS. +export const createIssueCorrectRange = (issuePosition: FileSuggestion, languages: IVSCodeLanguages): Range => { + return createIssueRange( + { + ...IssueUtils.createCorrectIssuePlacement(issuePosition), + }, + languages, + ); +}; + +export const updateFileReviewResultsPositions = ( + analysisResults: AnalysisResultLegacy, + updatedFile: openedTextEditorType, +): FilePath => { + const changesRange = updatedFile.contentChanges[0].range; + const changesText = updatedFile.contentChanges[0].text; + const goToNewLine = '\n'; + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + const offsetedline = changesRange.start.line + 1; + const charOffset = 1; + + const fileIssuesList = { + ...analysisResults.files[updatedFile.fullPath], + }; + for (const issue in fileIssuesList) { + if (!Object.prototype.hasOwnProperty.call(fileIssuesList, issue)) { + continue; + } + + for (const [index, position] of fileIssuesList[issue].entries()) { + const currentLineIsOnEdgeOfIssueRange = offsetedline === position.rows[0] || offsetedline === position.rows[1]; + + for (const row in position.rows) { + if (offsetedline < position.rows[row]) { + position.rows[row] += updatedFile.lineCount.prevOffset; + } else if (offsetedline === position.rows[row]) { + if (changesRange.start.character < position.rows[row]) { + position.rows[row] += updatedFile.lineCount.prevOffset; + } + } + } + + if (currentLineIsOnEdgeOfIssueRange || (offsetedline > position.rows[0] && offsetedline < position.rows[1])) { + // when chars are added + if (changesText.length && changesText !== goToNewLine && currentLineIsOnEdgeOfIssueRange) { + if (changesRange.start.character < position.cols[0] && !changesText.includes(goToNewLine)) { + for (const col in position.cols) { + if (!Object.prototype.hasOwnProperty.call(position.cols, col)) continue; + position.cols[col] += changesText.length; + } + } + // if char is inside issue range + if (changesRange.start.character >= position.cols[0] && changesRange.start.character <= position.cols[1]) { + position.cols[1] += changesText.length; + } + } + // when chars are deleted + if (updatedFile.contentChanges[0].rangeLength && currentLineIsOnEdgeOfIssueRange) { + if (updatedFile.lineCount.prevOffset < 0 && !changesText) { + continue; + } + if (changesRange.start.character < position.cols[0] && !changesText.includes(goToNewLine)) { + for (const char in position.cols) { + if (!Object.prototype.hasOwnProperty.call(position.cols, char)) continue; + position.cols[char] = + position.cols[char] > 0 ? position.cols[char] - updatedFile.contentChanges[0].rangeLength : 0; + } + } + // if char is in issue range + if (changesRange.start.character >= position.cols[0] && changesRange.start.character <= position.cols[1]) { + position.cols[1] = position.cols[1] > 0 ? position.cols[1] - updatedFile.contentChanges[0].rangeLength : 0; + } + } + // hide issue + if (position.cols[0] - charOffset === position.cols[1]) { + fileIssuesList[issue].splice(index, 1); + } + position.cols[0] = position.cols[0] > 0 ? position.cols[0] : 0; + position.cols[1] = position.cols[1] > 0 ? position.cols[1] : 0; + } + } + } + return fileIssuesList; +}; + +export const createIssueMarkerMsg = (originalMsg: string, [markerStartIdx, markerEndIdx]: number[]): string => { + return originalMsg.substring(markerStartIdx, markerEndIdx + 1); +}; + +export const createIssuesMarkersDecorationOptions = ( + currentFileReviewIssues: readonly Diagnostic[] | undefined, +): DecorationOptions[] => { + if (!currentFileReviewIssues) { + return []; + } + const issueMarkersDecorationOptions = currentFileReviewIssues.reduce((markersRanges, issue) => { + if (issue.relatedInformation) { + for (const markerInfo of issue.relatedInformation) { + markersRanges.push({ + range: markerInfo.location.range, + hoverMessage: markerInfo.message, + }); + } + } + return markersRanges; + }, Array()); + return issueMarkersDecorationOptions; +}; + +export const createIssueRelatedInformation = ( + markersList: Marker[], + fileUriPath: string, + message: string, + languages: IVSCodeLanguages, + workspace: IVSCodeWorkspace, + uriAdapter: IUriAdapter, +): DiagnosticRelatedInformation[] => { + return markersList.reduce((res, marker) => { + const { msg: markerMsgIdxs, pos: positions } = marker; + + positions.forEach(position => { + const positionUri = getAbsoluteMarkerFilePath(workspace, position.file, fileUriPath); + const relatedInfo = languages.createDiagnosticRelatedInformation( + uriAdapter.file(positionUri), + createIssueCorrectRange(position, languages), + createIssueMarkerMsg(message, markerMsgIdxs), + ); + res.push(relatedInfo); + }); + + return res; + }, Array()); +}; + +export const findCompleteSuggestion = ( + analysisResults: ISnykCodeResult, + suggestionId: string, + uri: Uri, + position: Range, + languages: IVSCodeLanguages, +): completeFileSuggestionType | undefined => { + const filePath = uri.fsPath; + if (!analysisResults.files[filePath]) return; + const file: FilePath = analysisResults.files[filePath]; + let fileSuggestion: FileSuggestion | undefined; + let suggestionIndex: string | number | undefined = Object.keys(file).find(i => { + const index = parseInt(i, 10); + if (analysisResults.suggestions[index].id !== suggestionId) return false; + const pos = file[index].find(fs => { + const r = createIssueCorrectRange(fs, languages); + return ( + r.start.character === position.start.character && + r.start.line === position.start.line && + r.end.character === position.end.character && + r.end.line === position.end.line + ); + }); + if (pos) { + fileSuggestion = pos; + return true; + } + return false; + }); + if (!fileSuggestion || !suggestionIndex) return; + suggestionIndex = parseInt(suggestionIndex, 10); + const suggestion = analysisResults.suggestions[suggestionIndex]; + if (!suggestion) return; + // eslint-disable-next-line consistent-return + return { + uri: uri.toString(), + ...suggestion, + ...fileSuggestion, + }; +}; + +export const checkCompleteSuggestion = ( + analysisResults: AnalysisResultLegacy, + suggestion: completeFileSuggestionType, + uriAdapter: IUriAdapter, +): boolean => { + const filePath = uriAdapter.parse(suggestion.uri).fsPath; + if (!analysisResults.files[filePath]) return false; + const file: FilePath = analysisResults.files[filePath]; + const suggestionIndex: string | undefined = Object.keys(file).find(i => { + const index = parseInt(i, 10); + if ( + analysisResults.suggestions[index].id !== suggestion.id || + analysisResults.suggestions[index].message !== suggestion.message + ) + return false; + const found = file[index].find(fs => { + let equal = true; + for (const dir of ['cols', 'rows']) { + for (const ind of [0, 1]) { + equal = equal && fs[dir][ind] === suggestion[dir][ind]; + } + } + return equal; + }); + return !!found; + }); + return !!suggestionIndex; +}; + +export const findSuggestionByMessage = ( + analysisResults: ISnykCodeResult, + suggestionName: string, +): ICodeSuggestion | undefined => { + return Object.values(analysisResults.suggestions).find( + (suggestion: ICodeSuggestion) => suggestion.message === suggestionName, + ); +}; export const ignoreIssueCommentText = (issueId: string, isFileIgnore?: boolean): string => { const snykComment = isFileIgnore ? FILE_IGNORE_ISSUE_BASE_COMMENT_TEXT : IGNORE_ISSUE_BASE_COMMENT_TEXT; return `${snykComment} ${issueId}: ${IGNORE_ISSUE_REASON_TIP}`; }; +export const isSecurityTypeSuggestion = (suggestion: Suggestion): boolean => { + return suggestion.categories.includes('Security'); +}; + export const getAbsoluteMarkerFilePath = ( workspace: IVSCodeWorkspace, markerFilePath: string, diff --git a/src/snyk/snykCode/utils/ignoreFileUtils.ts b/src/snyk/snykCode/utils/ignoreFileUtils.ts index 4ec63f1b7..96cf5e0e2 100644 --- a/src/snyk/snykCode/utils/ignoreFileUtils.ts +++ b/src/snyk/snykCode/utils/ignoreFileUtils.ts @@ -1,11 +1,10 @@ -import { CustomDCIgnore, DefaultDCIgnore } from '@deepcode/dcignore'; +import { constants } from '@snyk/code-client'; import { Buffer } from 'buffer'; import * as fs from 'fs'; import { IUriAdapter } from '../../common/vscode/uri'; import { IVSCodeWindow } from '../../common/vscode/window'; import { IVSCodeWorkspace } from '../../common/vscode/workspace'; -const DCIGNORE_FILENAME = '.dcignore'; export const createDCIgnore = async ( path: string, custom: boolean, @@ -13,8 +12,8 @@ export const createDCIgnore = async ( window: IVSCodeWindow, uriAdapter: IUriAdapter, ): Promise => { - const content: Buffer = Buffer.from(custom ? CustomDCIgnore : DefaultDCIgnore); - const filePath = `${path}/${DCIGNORE_FILENAME}`; + const content: Buffer = Buffer.from(custom ? constants.DCIGNORE_DRAFTS.custom : constants.DCIGNORE_DRAFTS.default); + const filePath = `${path}/${constants.DCIGNORE_FILENAME}`; const openPath = uriAdapter.file(filePath); // We don't want to override the dcignore file with an empty one. if (!custom || !fs.existsSync(filePath)) await workspace.fs.writeFile(openPath, content); diff --git a/src/snyk/snykCode/utils/issueUtils.ts b/src/snyk/snykCode/utils/issueUtils.ts index 2eebc1143..284c4d6bf 100644 --- a/src/snyk/snykCode/utils/issueUtils.ts +++ b/src/snyk/snykCode/utils/issueUtils.ts @@ -1,9 +1,33 @@ +import { FileSuggestion } from '@snyk/code-client'; import _ from 'lodash'; import { CodeIssueData, IssueSeverity } from '../../common/languageServer/types'; import { IVSCodeLanguages } from '../../common/vscode/languages'; -import { Range } from '../../common/vscode/types'; +import { Diagnostic, Position, Range } from '../../common/vscode/types'; + +export type IssuePlacementPosition = { + cols: { + start: number; + end: number; + }; + rows: { + start: number; + end: number; + }; +}; export class IssueUtils { + static findIssueWithRange = ( + matchingRange: Range | Position, + issuesList: readonly Diagnostic[] | undefined, + ): Diagnostic | undefined => { + return ( + issuesList && + issuesList.find((issue: Diagnostic) => { + return issue.range.contains(matchingRange); + }) + ); + }; + static issueSeverityAsText = (severity: IssueSeverity): 'Low' | 'Medium' | 'High' | 'Critical' => { return _.startCase(severity) as 'Low' | 'Medium' | 'High' | 'Critical'; }; @@ -26,12 +50,29 @@ export class IssueUtils { return isSecurityType ? 'Code Security Vulnerability' : 'Code Quality Issue'; }; + static createCorrectIssuePlacement = (item: FileSuggestion): IssuePlacementPosition => { + const rowOffset = 1; + const createPosition = (i: number): number => (i - rowOffset < 0 ? 0 : i - rowOffset); + return { + cols: { + start: createPosition(item.cols[0]), + end: item.cols[1], + }, + rows: { + start: createPosition(item.rows[0]), + end: createPosition(item.rows[1]), + }, + }; + }; + static createVsCodeRange = (issueData: CodeIssueData, languages: IVSCodeLanguages): Range => { return IssueUtils.createVsCodeRangeFromRange(issueData.rows, issueData.cols, languages); }; - // Creates zero-based range static createVsCodeRangeFromRange(rows: number[], cols: number[], languages: IVSCodeLanguages): Range { - return languages.createRange(rows[0], cols[0], rows[1], cols[1]); + const rowOffset = 1; + const createPosition = (i: number): number => (i - rowOffset < 0 ? 0 : i - rowOffset); + + return languages.createRange(createPosition(rows[0]), createPosition(cols[0]), createPosition(rows[1]), cols[1]); } } diff --git a/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewProvider.ts b/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewProvider.ts new file mode 100644 index 000000000..cd4541e70 --- /dev/null +++ b/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewProvider.ts @@ -0,0 +1,202 @@ +import { AnalysisSeverity } from '@snyk/code-client'; +import * as vscode from 'vscode'; +import { IAnalytics } from '../../../common/analytics/itly'; +import { SNYK_OPEN_BROWSER_COMMAND } from '../../../common/constants/commands'; +import { SNYK_VIEW_FALSE_POSITIVE_CODE } from '../../../common/constants/views'; +import { ErrorHandler } from '../../../common/error/errorHandler'; +import { ILog } from '../../../common/logger/interfaces'; +import { getNonce } from '../../../common/views/nonce'; +import { WebviewPanelSerializer } from '../../../common/views/webviewPanelSerializer'; +import { WebviewProvider } from '../../../common/views/webviewProvider'; +import { ExtensionContext } from '../../../common/vscode/extensionContext'; +import { IVSCodeWindow } from '../../../common/vscode/window'; +import { ISnykCodeServiceOld } from '../../codeServiceOld'; +import { FalsePositive } from '../../falsePositive/falsePositive'; +import { messages as errorMessages } from '../../messages/error'; + +enum FalsePositiveViewEventMessageType { + OpenBrowser = 'openBrowser', + Close = 'close', + Send = 'send', +} + +type FalsePositiveViewEventMessage = { + type: FalsePositiveViewEventMessageType; + value: unknown; +}; + +export type FalsePositiveWebviewModel = { + falsePositive: FalsePositive; + title: string; + severity: AnalysisSeverity; + severityText: string; + suggestionType: 'Issue' | 'Vulnerability'; + cwe: string[]; + isSecurityTypeIssue: boolean; +}; + +export class FalsePositiveWebviewProvider extends WebviewProvider { + constructor( + private readonly codeService: ISnykCodeServiceOld, + private readonly window: IVSCodeWindow, + protected readonly context: ExtensionContext, + protected readonly logger: ILog, + private readonly analytics: IAnalytics, + ) { + super(context, logger); + } + + activate(): void { + this.context.addDisposables( + this.window.registerWebviewPanelSerializer(SNYK_VIEW_FALSE_POSITIVE_CODE, new WebviewPanelSerializer(this)), + ); + } + + async showPanel(model: FalsePositiveWebviewModel): Promise { + try { + if (this.panel) { + this.panel.reveal(vscode.ViewColumn.One, true); + } else { + this.panel = vscode.window.createWebviewPanel( + SNYK_VIEW_FALSE_POSITIVE_CODE, + 'Snyk Report False Positive', + { + viewColumn: vscode.ViewColumn.One, + preserveFocus: true, + }, + this.getWebviewOptions(), + ); + this.registerListeners(); + } + + this.panel.webview.html = this.getHtmlForWebview(this.panel.webview); + await this.panel.webview.postMessage({ type: 'set', args: model }); + + this.analytics.logFalsePositiveIsDisplayed(); + } catch (e) { + ErrorHandler.handle(e, this.logger, errorMessages.reportFalsePositiveViewShowFailed); + } + } + + protected registerListeners() { + if (!this.panel) return; + + this.panel.onDidDispose(() => this.onPanelDispose(), null, this.disposables); + this.panel.webview.onDidReceiveMessage( + async (data: FalsePositiveViewEventMessage) => { + switch (data.type) { + case FalsePositiveViewEventMessageType.Send: + // eslint-disable-next-line no-case-declarations + const { falsePositive, content, isSecurityTypeIssue, issueSeverity } = data.value as { + falsePositive: FalsePositive; + content: string; + isSecurityTypeIssue: boolean; + issueSeverity: AnalysisSeverity; + }; + await this.reportFalsePositive(falsePositive, content, isSecurityTypeIssue, issueSeverity); + break; + case FalsePositiveViewEventMessageType.OpenBrowser: + void vscode.commands.executeCommand(SNYK_OPEN_BROWSER_COMMAND, data.value); + break; + case FalsePositiveViewEventMessageType.Close: + this.disposePanel(); + break; + default: + break; + } + }, + null, + this.disposables, + ); + this.panel.onDidChangeViewState(() => this.checkVisibility(), null, this.disposables); + } + + private async reportFalsePositive( + falsePositive: FalsePositive, + content: string, + isSecurityTypeIssue: boolean, + issueSeverity: AnalysisSeverity, + ): Promise { + falsePositive.content = content; + await this.codeService.reportFalsePositive(falsePositive, isSecurityTypeIssue, issueSeverity); + } + + protected getHtmlForWebview(webview: vscode.Webview): string { + const images: Record = [ + ['dark-critical-severity', 'svg'], + ['dark-high-severity', 'svg'], + ['dark-medium-severity', 'svg'], + ['dark-low-severity', 'svg'], + ['warning', 'svg'], + ].reduce((accumulator: Record, [name, ext]) => { + const uri = this.getWebViewUri('media', 'images', `${name}.${ext}`); + if (!uri) throw new Error('Image missing.'); + accumulator[name] = uri.toString(); + return accumulator; + }, {}); + + const scriptUri = this.getWebViewUri( + 'out', + 'snyk', + 'snykCode', + 'views', + 'falsePositive', + 'falsePositiveWebviewScript.js', + ); + const styleUri = this.getWebViewUri('media', 'views', 'snykCode', 'falsePositive', 'falsePositive.css'); + + const nonce = getNonce(); + + return ` + + + + + + + + + + + +
+
+
+ + + + + + + + + +
+
+
+
+
+ +
+
+
+ + Please check the code. It will be uploaded to Snyk and manually reviewed by our engineers. +
+
+
+
+ + +
+
+ + + + + `; + } +} diff --git a/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewScript.ts b/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewScript.ts new file mode 100644 index 000000000..e736b141f --- /dev/null +++ b/src/snyk/snykCode/views/falsePositive/falsePositiveWebviewScript.ts @@ -0,0 +1,148 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/// + +// This script will be run within the webview itself +// It cannot access the main VS Code APIs directly. +(function () { + // TODO: Redefine types until bundling is introduced into extension + // https://stackoverflow.com/a/56938089/1713082 + type FalsePositiveWebviewModel = { + falsePositive: { + content: string; + }; + title: string; + severity: number; + severityText: string; + suggestionType: 'Issue' | 'Vulnerability'; + cwe: string[]; + isSecurityTypeIssue: boolean; + }; + + const vscode = acquireVsCodeApi(); + let model = {} as FalsePositiveWebviewModel; + + function send() { + const editor = document.querySelector('.editor') as HTMLTextAreaElement; + + vscode.postMessage({ + type: 'send', + value: { + falsePositive: model.falsePositive, + content: editor.value, + isSecurityTypeIssue: model.isSecurityTypeIssue, + issueSeverity: model.severity, + }, + }); + } + + function navigateToUrl(url: string) { + vscode.postMessage({ + type: 'openBrowser', + value: url, + }); + } + + function close() { + vscode.postMessage({ + type: 'close', + }); + } + + function showFalsePositive() { + const severity = document.querySelector('.severity')!; + const title = document.querySelector('.suggestion .suggestion-text')!; + + // Set title + title.innerHTML = model.title; + + // Set severity icon + setSeverityIcon(); + + // Fill identifiers line + fillIdentifiers(); + + // Set editor code + setEditorCode(); + + function setSeverityIcon() { + if (model.severityText) { + severity.querySelectorAll('img').forEach(n => { + if (n.id.slice(-1) === 'l') { + if (n.id.includes(model.severityText)) n.className = 'icon light-only'; + else n.className = 'icon light-only hidden'; + } else { + if (n.id.includes(model.severityText)) n.className = 'icon dark-only'; + else n.className = 'icon dark-only hidden'; + } + }); + } else { + severity.querySelectorAll('img').forEach(n => (n.className = 'icon hidden')); + } + } + + function fillIdentifiers() { + const identifiers = document.querySelector('.identifiers')!; + identifiers.innerHTML = ''; // reset node + + const type = model.suggestionType; + const typeNode = document.createTextNode(type); + identifiers.appendChild(typeNode); + + model.cwe.forEach(cwe => appendIdentifierSpan(identifiers, cwe, getCweLink(cwe))); + } + + function appendIdentifierSpan(identifiers: Element, id: string, link?: string) { + const delimiter = document.createElement('span'); + delimiter.innerText = ' | '; + delimiter.className = 'delimiter'; + identifiers.appendChild(delimiter); + + let cveNode: HTMLElement; + if (link) { + cveNode = document.createElement('a'); + cveNode.onclick = () => navigateToUrl(link); + } else { + cveNode = document.createElement('span'); + } + + cveNode.innerText = id; + + identifiers.appendChild(cveNode); + } + + function getCweLink(cwe: string) { + const id = cwe.toUpperCase().replace('CWE-', ''); + return `https://cwe.mitre.org/data/definitions/${id}.html`; + } + + function setEditorCode() { + const editor = document.querySelector('.editor')!; + editor.textContent = model.falsePositive.content; + } + } + + document.getElementById('cancel')?.addEventListener('click', close); + document.getElementById('send')?.addEventListener('click', send); + + // deepcode ignore InsufficientValidation: Content Security Policy applied in provider + window.addEventListener('message', event => { + const { type, args } = event.data; + switch (type) { + case 'set': { + model = args; + vscode.setState(model); + break; + } + case 'get': { + model = vscode.getState(); + break; + } + } + + showFalsePositive(); + }); +})(); diff --git a/src/snyk/snykCode/views/interfaces.ts b/src/snyk/snykCode/views/interfaces.ts index 4ea9289da..889cf2160 100644 --- a/src/snyk/snykCode/views/interfaces.ts +++ b/src/snyk/snykCode/views/interfaces.ts @@ -1,11 +1,24 @@ import * as vscode from 'vscode'; import { CodeIssueData, Issue } from '../../common/languageServer/types'; import { IWebViewProvider } from '../../common/views/webviewProvider'; +import { completeFileSuggestionType } from '../interfaces'; + +export interface ICodeSuggestionWebviewProviderOld extends IWebViewProvider { + show(suggestionId: string, uri: vscode.Uri, position: vscode.Range): void; + checkCurrentSuggestion(): void; +} export interface ICodeSuggestionWebviewProvider extends IWebViewProvider> { openIssueId: string | undefined; } +export type CodeIssueCommandArgOld = { + message: string; + filePath: vscode.Uri; + range: vscode.Range; + diagnostic: vscode.Diagnostic; +}; + export type CodeIssueCommandArg = { id: string; folderPath: string; diff --git a/src/snyk/snykCode/views/issueTreeProvider.ts b/src/snyk/snykCode/views/issueTreeProvider.ts index 419181efa..91f69dd9a 100644 --- a/src/snyk/snykCode/views/issueTreeProvider.ts +++ b/src/snyk/snykCode/views/issueTreeProvider.ts @@ -44,6 +44,7 @@ export class IssueTreeProvider extends AnalysisTreeNodeProvder { getRootChildren(): TreeNode[] { const nodes: TreeNode[] = []; + if (!this.contextService.shouldShowCodeAnalysis) return nodes; if (!this.codeService.isLsDownloadSuccessful) { return [this.getErrorEncounteredTreeNode()]; } @@ -69,12 +70,6 @@ export class IssueTreeProvider extends AnalysisTreeNodeProvder { const [resultNodes, nIssues] = this.getResultNodes(); nodes.push(...resultNodes); - const folderResults = Array.from(this.codeService.result.values()); - const allFailed = folderResults.every(folderResult => folderResult instanceof Error); - if (allFailed) { - return nodes; - } - nodes.sort(this.compareNodes); const topNodes = [ diff --git a/src/snyk/snykCode/views/issueTreeProviderOld.ts b/src/snyk/snykCode/views/issueTreeProviderOld.ts new file mode 100644 index 000000000..2387d24c3 --- /dev/null +++ b/src/snyk/snykCode/views/issueTreeProviderOld.ts @@ -0,0 +1,188 @@ +import { Command, Diagnostic, DiagnosticCollection, Range, Uri } from 'vscode'; +import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; +import { IContextService } from '../../common/services/contextService'; +import { AnalysisTreeNodeProvder } from '../../common/views/analysisTreeNodeProvider'; +import { INodeIcon, NODE_ICONS, TreeNode } from '../../common/views/treeNode'; +import { ISnykCodeServiceOld } from '../codeServiceOld'; +import { SNYK_SEVERITIES } from '../constants/analysis'; +import { messages } from '../messages/analysis'; +import { getSnykSeverity } from '../utils/analysisUtils'; +import { CodeIssueCommandArgOld } from './interfaces'; + +interface ISeverityCounts { + [severity: number]: number; +} + +export class IssueTreeProviderOld extends AnalysisTreeNodeProvder { + constructor( + protected contextService: IContextService, + protected snykCode: ISnykCodeServiceOld, + protected diagnosticCollection: DiagnosticCollection | undefined, + protected configuration: IConfiguration, + ) { + super(configuration, snykCode); + } + + static getSeverityIcon(severity: number): INodeIcon { + return ( + { + [SNYK_SEVERITIES.error]: NODE_ICONS.high, + [SNYK_SEVERITIES.warning]: NODE_ICONS.medium, + [SNYK_SEVERITIES.information]: NODE_ICONS.low, + }[severity] || NODE_ICONS.low + ); + } + + static getFileSeverity(counts: ISeverityCounts): number { + for (const s of [SNYK_SEVERITIES.error, SNYK_SEVERITIES.warning, SNYK_SEVERITIES.information]) { + if (counts[s]) return s; + } + return SNYK_SEVERITIES.information; + } + + getRootChildren(): TreeNode[] { + const review: TreeNode[] = []; + let nIssues = 0; + if (!this.contextService.shouldShowCodeAnalysis) return review; + + if (this.snykCode.hasTransientError) { + return this.getTransientErrorTreeNodes(); + } else if (this.snykCode.hasError) { + return [this.getErrorEncounteredTreeNode()]; + } else if (!this.snykCode.isAnyWorkspaceFolderTrusted) { + return [this.getNoWorkspaceTrustTreeNode()]; + } + + if (this.diagnosticCollection) { + this.diagnosticCollection.forEach((uri: Uri, diagnostics: readonly Diagnostic[]): void => { + const filePath = uri.path.split('/'); + const filename = filePath.pop() || uri.path; + const dir = filePath.pop(); + + nIssues += diagnostics.length; + + if (diagnostics.length == 0) return; + + const [issues, severityCounts] = this.getVulnerabilityTreeNodes(diagnostics, uri); + issues.sort(this.compareNodes); + const fileSeverity = IssueTreeProviderOld.getFileSeverity(severityCounts); + const file = new TreeNode({ + text: filename, + description: this.getIssueDescriptionText(dir, diagnostics), + icon: IssueTreeProviderOld.getSeverityIcon(fileSeverity), + children: issues, + internal: { + nIssues: diagnostics.length, + severity: fileSeverity, + }, + }); + review.push(file); + }); + } + review.sort(this.compareNodes); + if (this.snykCode.isAnalysisRunning) { + review.unshift( + new TreeNode({ + text: this.snykCode.analysisStatus, + description: this.snykCode.analysisProgress, + }), + ); + } else { + const topNodes = [ + new TreeNode({ + text: this.getIssueFoundText(nIssues), + }), + this.getDurationTreeNode(), + this.getNoSeverityFiltersSelectedTreeNode(), + ]; + review.unshift(...topNodes.filter((n): n is TreeNode => n !== null)); + } + return review; + } + + protected getIssueFoundText(nIssues: number): string { + return `Snyk found ${!nIssues ? 'no issues! ✅' : `${nIssues} issue${nIssues === 1 ? '' : 's'}`}`; + } + + protected getIssueDescriptionText(dir: string | undefined, diagnostics: readonly Diagnostic[]): string | undefined { + return `${dir} - ${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}`; + } + + protected getFilteredIssues(diagnostics: readonly Diagnostic[]): readonly Diagnostic[] { + // Diagnostics are already filtered by the analyzer + return diagnostics; + } + + private getVulnerabilityTreeNodes( + fileVulnerabilities: readonly Diagnostic[], + uri: Uri, + ): [TreeNode[], ISeverityCounts] { + const severityCounts: ISeverityCounts = { + [SNYK_SEVERITIES.information]: 0, + [SNYK_SEVERITIES.warning]: 0, + [SNYK_SEVERITIES.error]: 0, + }; + + const nodes = fileVulnerabilities.map(d => { + const severity = getSnykSeverity(d.severity); + severityCounts[severity] += 1; + const params: { + text: string; + icon: INodeIcon; + issue: { uri: Uri; filePath: string; range?: Range }; + internal: { severity: number }; + command: Command; + children?: TreeNode[]; + } = { + text: d.message, + icon: IssueTreeProviderOld.getSeverityIcon(severity), + issue: { + uri, + filePath: 'dummy', // todo: consolidate uri to filePath + range: d.range, + }, + internal: { + severity, + }, + command: { + command: SNYK_OPEN_ISSUE_COMMAND, + title: '', + arguments: [ + { + issueType: OpenCommandIssueType.CodeIssueOld, + issue: { + message: d.message, + filePath: uri, + range: d.range, + diagnostic: d, + } as CodeIssueCommandArgOld, + } as OpenIssueCommandArg, + ], + }, + }; + + return new TreeNode(params); + }); + + return [nodes, severityCounts]; + } + + private getTransientErrorTreeNodes(): TreeNode[] { + return [ + new TreeNode({ + text: messages.temporaryFailed, + internal: { + isError: true, + }, + }), + new TreeNode({ + text: messages.retry, + internal: { + isError: true, + }, + }), + ]; + } +} diff --git a/src/snyk/snykCode/views/qualityIssueTreeProvider.ts b/src/snyk/snykCode/views/qualityIssueTreeProvider.ts index 112fe7558..cf3afd308 100644 --- a/src/snyk/snykCode/views/qualityIssueTreeProvider.ts +++ b/src/snyk/snykCode/views/qualityIssueTreeProvider.ts @@ -1,6 +1,6 @@ import { IConfiguration } from '../../common/configuration/configuration'; import { configuration } from '../../common/configuration/instance'; -import { SNYK_SCAN_STATUS } from '../../common/constants/views'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; import { IContextService } from '../../common/services/contextService'; import { IViewManagerService } from '../../common/services/viewManagerService'; import { TreeNode } from '../../common/views/treeNode'; @@ -23,7 +23,7 @@ export class CodeQualityIssueTreeProvider extends IssueTreeProvider { if (!configuration.getFeaturesConfiguration()?.codeQualityEnabled) { return [ new TreeNode({ - text: SNYK_SCAN_STATUS.CODE_QUALITY_DISABLED, + text: SNYK_ANALYSIS_STATUS.CODE_QUALITY_DISABLED, }), ]; } diff --git a/src/snyk/snykCode/views/qualityIssueTreeProviderOld.ts b/src/snyk/snykCode/views/qualityIssueTreeProviderOld.ts new file mode 100644 index 000000000..fffd598b8 --- /dev/null +++ b/src/snyk/snykCode/views/qualityIssueTreeProviderOld.ts @@ -0,0 +1,33 @@ +import { IConfiguration } from '../../common/configuration/configuration'; +import { configuration } from '../../common/configuration/instance'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; +import { IContextService } from '../../common/services/contextService'; +import { IViewManagerService } from '../../common/services/viewManagerService'; +import { TreeNode } from '../../common/views/treeNode'; +import { ISnykCodeServiceOld } from '../codeServiceOld'; +import { IssueTreeProviderOld } from './issueTreeProviderOld'; + +export class CodeQualityIssueTreeProviderOld extends IssueTreeProviderOld { + constructor( + protected viewManagerService: IViewManagerService, + protected contextService: IContextService, + protected snykCode: ISnykCodeServiceOld, + protected configuration: IConfiguration, + ) { + super(contextService, snykCode, snykCode.analyzer.codeQualityReview, configuration); + } + + getRootChildren(): TreeNode[] { + if (!configuration.getFeaturesConfiguration()?.codeQualityEnabled) { + return [ + new TreeNode({ + text: SNYK_ANALYSIS_STATUS.CODE_QUALITY_DISABLED, + }), + ]; + } + + return super.getRootChildren(); + } + + onDidChangeTreeData = this.viewManagerService.refreshOldCodeQualityViewEmitter.event; +} diff --git a/src/snyk/snykCode/views/securityIssueTreeProvider.ts b/src/snyk/snykCode/views/securityIssueTreeProvider.ts index 81f653642..09e1b1533 100644 --- a/src/snyk/snykCode/views/securityIssueTreeProvider.ts +++ b/src/snyk/snykCode/views/securityIssueTreeProvider.ts @@ -1,6 +1,6 @@ import { IConfiguration } from '../../common/configuration/configuration'; import { configuration } from '../../common/configuration/instance'; -import { SNYK_SCAN_STATUS } from '../../common/constants/views'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; import { IContextService } from '../../common/services/contextService'; import { IViewManagerService } from '../../common/services/viewManagerService'; import { TreeNode } from '../../common/views/treeNode'; @@ -23,7 +23,7 @@ export default class CodeSecurityIssueTreeProvider extends IssueTreeProvider { if (!configuration.getFeaturesConfiguration()?.codeSecurityEnabled) { return [ new TreeNode({ - text: SNYK_SCAN_STATUS.CODE_SECURITY_DISABLED, + text: SNYK_ANALYSIS_STATUS.CODE_SECURITY_DISABLED, }), ]; } diff --git a/src/snyk/snykCode/views/securityIssueTreeProviderOld.ts b/src/snyk/snykCode/views/securityIssueTreeProviderOld.ts new file mode 100644 index 000000000..34ea82c89 --- /dev/null +++ b/src/snyk/snykCode/views/securityIssueTreeProviderOld.ts @@ -0,0 +1,44 @@ +import { Diagnostic } from 'vscode'; +import { IConfiguration } from '../../common/configuration/configuration'; +import { configuration } from '../../common/configuration/instance'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; +import { IContextService } from '../../common/services/contextService'; +import { IViewManagerService } from '../../common/services/viewManagerService'; +import { TreeNode } from '../../common/views/treeNode'; +import { ISnykCodeServiceOld } from '../codeServiceOld'; +import { IssueTreeProviderOld } from './issueTreeProviderOld'; + +export class CodeSecurityIssueTreeProviderOld extends IssueTreeProviderOld { + constructor( + protected viewManagerService: IViewManagerService, + protected contextService: IContextService, + protected snykCode: ISnykCodeServiceOld, + protected configuration: IConfiguration, + ) { + super(contextService, snykCode, snykCode.analyzer.codeSecurityReview, configuration); + } + + getRootChildren(): TreeNode[] { + if (!configuration.getFeaturesConfiguration()?.codeSecurityEnabled) { + return [ + new TreeNode({ + text: SNYK_ANALYSIS_STATUS.CODE_SECURITY_DISABLED, + }), + ]; + } + + return super.getRootChildren(); + } + + onDidChangeTreeData = this.viewManagerService.refreshOldCodeSecurityViewEmitter.event; + + protected getIssueDescriptionText(dir: string | undefined, diagnostics: readonly Diagnostic[]): string | undefined { + return `${dir} - ${diagnostics.length} ${diagnostics.length === 1 ? 'vulnerability' : 'vulnerabilities'}`; + } + + protected getIssueFoundText(nIssues: number): string { + return `Snyk found ${ + !nIssues ? 'no vulnerabilities! ✅' : `${nIssues} ${nIssues === 1 ? 'vulnerability' : 'vulnerabilities'}` + }`; + } +} diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts index 9bc4a58b5..98a5d4300 100644 --- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import * as vscode from 'vscode'; import { OpenCommandIssueType } from '../../../common/commands/types'; import { @@ -25,10 +24,16 @@ import { getAbsoluteMarkerFilePath } from '../../utils/analysisUtils'; import { IssueUtils } from '../../utils/issueUtils'; import { ICodeSuggestionWebviewProvider } from '../interfaces'; +export declare enum AnalysisSeverity { + info = 1, + warning = 2, + critical = 3, +} + type Suggestion = { id: string; message: string; - severity: string; + severity: AnalysisSeverity; leadURL?: string; rule: string; repoDatasetSize: number; @@ -142,11 +147,25 @@ export class CodeSuggestionWebviewProvider } private mapToModel(issue: Issue): Suggestion { + // TODO: avoid mapping severity to ints. Remove when releasing Code scans using LS & update codeSuggestionWebviewScript.ts respectively. + let severity; + switch (issue.severity) { + case 'low': + severity = 1; + break; + case 'medium': + severity = 2; + break; + default: + severity = 3; + break; + } + return { id: issue.id, title: issue.title, + severity, uri: issue.filePath, - severity: _.capitalize(issue.severity), ...issue.additionalData, }; } diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProviderOld.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProviderOld.ts new file mode 100644 index 000000000..cfcc4184f --- /dev/null +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProviderOld.ts @@ -0,0 +1,305 @@ +import * as vscode from 'vscode'; +import { OpenCommandIssueType } from '../../../common/commands/types'; +import { IConfiguration } from '../../../common/configuration/configuration'; +import { + SNYK_IGNORE_ISSUE_COMMAND, + SNYK_OPEN_BROWSER_COMMAND, + SNYK_OPEN_LOCAL_COMMAND, +} from '../../../common/constants/commands'; +import { SNYK_VIEW_SUGGESTION_CODE_OLD } from '../../../common/constants/views'; +import { ErrorHandler } from '../../../common/error/errorHandler'; +import { ILog } from '../../../common/logger/interfaces'; +import { messages as learnMessages } from '../../../common/messages/learn'; +import { LearnService } from '../../../common/services/learnService'; +import { getNonce } from '../../../common/views/nonce'; +import { WebviewPanelSerializer } from '../../../common/views/webviewPanelSerializer'; +import { WebviewProvider } from '../../../common/views/webviewProvider'; +import { ExtensionContext } from '../../../common/vscode/extensionContext'; +import { IVSCodeLanguages } from '../../../common/vscode/languages'; +import { IVSCodeWindow } from '../../../common/vscode/window'; +import { IVSCodeWorkspace } from '../../../common/vscode/workspace'; +import { ICodeSettings } from '../../codeSettings'; +import { WEBVIEW_PANEL_QUALITY_TITLE, WEBVIEW_PANEL_SECURITY_TITLE } from '../../constants/analysis'; +import { completeFileSuggestionType, ISnykCodeAnalyzer } from '../../interfaces'; +import { messages as errorMessages } from '../../messages/error'; +import { createIssueCorrectRange, getAbsoluteMarkerFilePath, getVSCodeSeverity } from '../../utils/analysisUtils'; +import { ICodeSuggestionWebviewProviderOld } from '../interfaces'; + +export class CodeSuggestionWebviewProviderOld + extends WebviewProvider + implements ICodeSuggestionWebviewProviderOld +{ + // For consistency reasons, the single source of truth for the current suggestion is the + // panel state. The following field is only used in + private suggestion: completeFileSuggestionType | undefined; + + constructor( + private readonly configuration: IConfiguration, + private readonly analyzer: ISnykCodeAnalyzer, + private readonly window: IVSCodeWindow, + protected readonly context: ExtensionContext, + protected readonly logger: ILog, + private readonly languages: IVSCodeLanguages, + private readonly workspace: IVSCodeWorkspace, + private readonly codeSettings: ICodeSettings, + private readonly learnService: LearnService, + ) { + super(context, logger); + } + + activate(): void { + this.context.addDisposables( + this.window.registerWebviewPanelSerializer(SNYK_VIEW_SUGGESTION_CODE_OLD, new WebviewPanelSerializer(this)), + ); + } + + show(suggestionId: string, uri: vscode.Uri, position: vscode.Range): void { + const suggestion = this.analyzer.getFullSuggestion(suggestionId, uri, position); + if (!suggestion) { + this.disposePanel(); + return; + } + + void this.showPanel(suggestion); + } + + checkCurrentSuggestion(): void { + if (!this.panel || !this.suggestion) return; + const found = this.analyzer.checkFullSuggestion(this.suggestion); + if (!found) this.disposePanel(); + } + + async postLearnLessonMessage(suggestion: completeFileSuggestionType): Promise { + try { + if (this.panel) { + const lesson = await this.learnService.getLesson(suggestion, OpenCommandIssueType.CodeIssueOld); + if (lesson) { + void this.panel.webview.postMessage({ + type: 'setLesson', + args: { url: lesson.url, title: learnMessages.lessonButtonTitle }, + }); + } else { + void this.panel.webview.postMessage({ + type: 'setLesson', + args: null, + }); + } + } + } catch (e) { + ErrorHandler.handle(e, this.logger, learnMessages.getLessonError); + } + } + + async showPanel(suggestion: completeFileSuggestionType): Promise { + try { + await this.focusSecondEditorGroup(); + + if (this.panel) { + this.panel.title = this.getTitle(suggestion); + this.panel.reveal(vscode.ViewColumn.Two, true); + } else { + this.panel = vscode.window.createWebviewPanel( + SNYK_VIEW_SUGGESTION_CODE_OLD, + this.getTitle(suggestion), + { + viewColumn: vscode.ViewColumn.Two, + preserveFocus: true, + }, + this.getWebviewOptions(), + ); + this.registerListeners(); + } + + this.panel.webview.html = this.getHtmlForWebview(this.panel.webview); + + await this.panel.webview.postMessage({ type: 'set', args: suggestion }); + void this.postLearnLessonMessage(suggestion); + + this.suggestion = suggestion; + } catch (e) { + ErrorHandler.handle(e, this.logger, errorMessages.suggestionViewShowFailed); + } + } + + protected registerListeners(): void { + if (!this.panel) return; + + this.panel.onDidDispose(() => this.onPanelDispose(), null, this.disposables); + this.panel.onDidChangeViewState(() => this.checkVisibility(), undefined, this.disposables); + // Handle messages from the webview + this.panel.webview.onDidReceiveMessage(msg => this.handleMessage(msg), undefined, this.disposables); + } + + disposePanel(): void { + super.disposePanel(); + } + + protected onPanelDispose(): void { + super.onPanelDispose(); + } + + private async handleMessage(message: any) { + try { + const { type, args } = message; + switch (type) { + case 'openLocal': { + const { uri, cols, rows, suggestionUri } = args as { + uri: string; + cols: [number, number]; + rows: [number, number]; + suggestionUri: string; + }; + const localUriPath = getAbsoluteMarkerFilePath(this.workspace, uri, suggestionUri); + const localUri = vscode.Uri.parse(localUriPath); + const range = createIssueCorrectRange({ cols, rows }, this.languages); + await vscode.commands.executeCommand(SNYK_OPEN_LOCAL_COMMAND, localUri, range); + break; + } + case 'openBrowser': { + const { url } = args; + await vscode.commands.executeCommand(SNYK_OPEN_BROWSER_COMMAND, url); + break; + } + case 'ignoreIssue': { + // eslint-disable-next-line no-shadow + let { lineOnly, message, rule, uri, cols, rows } = args; + uri = vscode.Uri.parse(uri as string); + const range = createIssueCorrectRange({ cols, rows }, this.languages); + await vscode.commands.executeCommand(SNYK_IGNORE_ISSUE_COMMAND, { + uri, + matchedIssue: { message, range }, + ruleId: rule, + isFileIgnore: !lineOnly, + }); + this.panel?.dispose(); + break; + } + default: { + throw new Error('Unknown message type'); + } + } + } catch (e) { + ErrorHandler.handle(e, this.logger, errorMessages.suggestionViewMessageHandlingFailed(JSON.stringify(message))); + } + } + + private getTitle(suggestion: completeFileSuggestionType): string { + return suggestion.isSecurityType ? WEBVIEW_PANEL_SECURITY_TITLE : WEBVIEW_PANEL_QUALITY_TITLE; + } + + protected getHtmlForWebview(webview: vscode.Webview): string { + const images: Record = [ + ['icon-lines', 'svg'], + ['icon-external', 'svg'], + ['icon-code', 'svg'], + ['icon-github', 'svg'], + ['icon-like', 'svg'], + ['dark-high-severity', 'svg'], + ['dark-medium-severity', 'svg'], + ['light-icon-critical', 'svg'], + ['dark-low-severity', 'svg'], + ['arrow-left-dark', 'svg'], + ['arrow-right-dark', 'svg'], + ['arrow-left-light', 'svg'], + ['arrow-right-light', 'svg'], + ['learn-icon', 'svg'], + ].reduce>((accumulator: Record, [name, ext]) => { + const uri = this.getWebViewUri('media', 'images', `${name}.${ext}`); + if (!uri) throw new Error('Image missing.'); + accumulator[name] = uri.toString(); + return accumulator; + }, {}); + + const scriptUri = this.getWebViewUri( + 'out', + 'snyk', + 'snykCode', + 'views', + 'suggestion', + 'codeSuggestionWebviewScript.js', + ); + const styleUri = this.getWebViewUri('media', 'views', 'snykCode', 'suggestion', 'suggestion.css'); + const styleVSCodeUri = this.getWebViewUri('media', 'views', 'common', 'vscode.css'); + const learnStyleUri = this.getWebViewUri('media', 'views', 'common', 'learn.css'); + + const nonce = getNonce(); + return ` + + + + + + + + + + + + +
+
+
+ + + + + + + +
+
+ +
+ + +
+
+
+
+ This issue was fixed by projects. Here are example fixes. +
+
+ There are no example fixes for this issue. +
+
+
+ + +
+
+
+ + +
+ + Example 1/ + +
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + `; + } +} diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts index 9bc44c6b7..a60d27895 100644 --- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts @@ -115,14 +115,14 @@ } function getCurrentSeverity() { const stringMap = { - Low: 1, - Medium: 2, - High: 3, + 1: 'Low', + 2: 'Medium', + 3: 'High', }; return suggestion ? { - value: stringMap[suggestion.severity], - text: suggestion.severity, + value: suggestion.severity, + text: stringMap[suggestion.severity], } : undefined; } @@ -190,8 +190,7 @@ let markLineText = ' ['; let first = true; for (const p of m.pos) { - const rowStart = Number(p.rows[0]) + 1; // editors are 1-based - markLineText += (first ? '' : ', ') + ':' + rowStart.toString(); + markLineText += (first ? '' : ', ') + ':' + (p.rows[0] as string); first = false; } markLineText += ']'; @@ -212,9 +211,9 @@ moreInfo.className = suggestion.leadURL ? 'clickable' : 'clickable hidden'; const suggestionPosition = document.getElementById('line-position')!; - suggestionPosition.innerHTML = (Number(suggestion.rows[0]) + 1).toString(); // editors are 1-based + suggestionPosition.innerHTML = suggestion.rows[0]; const suggestionPosition2 = document.getElementById('line-position2')!; - suggestionPosition2.innerHTML = (Number(suggestion.rows[0]) + 1).toString(); + suggestionPosition2.innerHTML = suggestion.rows[0]; const dataset = document.getElementById('dataset-number')!; const infoTop = document.getElementById('info-top')!; diff --git a/src/snyk/snykCode/watchers/editorsWatcher.ts b/src/snyk/snykCode/watchers/editorsWatcher.ts new file mode 100644 index 000000000..1576c0599 --- /dev/null +++ b/src/snyk/snykCode/watchers/editorsWatcher.ts @@ -0,0 +1,80 @@ +import * as vscode from 'vscode'; +import { IExtension } from '../../base/modules/interfaces'; +import { IWatcher } from '../../common/watchers/interfaces'; +import { openedTextEditorType } from '../interfaces'; + +class SnykEditorsWatcher implements IWatcher { + private currentTextEditors: { + [key: string]: openedTextEditorType; + } = {}; + + private createEditorInfo(editor: vscode.TextEditor): void { + const path = editor.document.fileName; + + const workspacePath = (vscode.workspace.workspaceFolders || []) + .map(f => f.uri.fsPath) + .find(p => editor.document.fileName.includes(p)); + + this.currentTextEditors[editor.document.fileName] = { + fullPath: path, + workspace: workspacePath || '', + lineCount: { + current: editor.document.lineCount, + prevOffset: 0, + }, + contentChanges: [], + document: editor.document, + }; + } + + private watchEditorsNavChange() { + vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor | undefined) => { + if (editor && !this.currentTextEditors[editor.document.fileName]) { + this.createEditorInfo(editor); + } + }); + } + + private watchClosingEditor() { + vscode.workspace.onDidCloseTextDocument((document: vscode.TextDocument) => { + delete this.currentTextEditors[document.fileName]; + }); + } + + private watchEditorCodeChanges(extension: IExtension) { + vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => { + const currentEditorFileName = event.document.fileName; + if (this.currentTextEditors[currentEditorFileName] && event.contentChanges && event.contentChanges.length) { + const curentLineCount = this.currentTextEditors[currentEditorFileName].lineCount.current; + this.currentTextEditors[currentEditorFileName] = { + ...this.currentTextEditors[currentEditorFileName], + lineCount: { + current: event.document.lineCount, + prevOffset: event.document.lineCount - curentLineCount, + }, + contentChanges: [...event.contentChanges], + document: event.document, + }; + void extension.snykCodeOld.analyzer.updateReviewResultsPositions( + extension, + this.currentTextEditors[currentEditorFileName], + ); + } + }); + } + + private async prepareWatchers(extension: IExtension): Promise { + for await (const editor of vscode.window.visibleTextEditors) { + this.createEditorInfo(editor); + } + this.watchEditorsNavChange(); + this.watchClosingEditor(); + this.watchEditorCodeChanges(extension); + } + + public activate(extension: IExtension): void { + void this.prepareWatchers(extension); + } +} + +export default SnykEditorsWatcher; diff --git a/src/snyk/snykCode/watchers/filesWatcher.ts b/src/snyk/snykCode/watchers/filesWatcher.ts new file mode 100644 index 000000000..c0cd957c5 --- /dev/null +++ b/src/snyk/snykCode/watchers/filesWatcher.ts @@ -0,0 +1,30 @@ +import { getGlobPatterns, SupportedFiles } from '@snyk/code-client'; +import * as vscode from 'vscode'; +import { IExtension } from '../../base/modules/interfaces'; +import { IVSCodeWorkspace } from '../../common/vscode/workspace'; + +export default function createFileWatcher( + extension: IExtension, + workspace: IVSCodeWorkspace, + supportedFiles: SupportedFiles, +): vscode.FileSystemWatcher { + const globPattern: vscode.GlobPattern = `**/{${getGlobPatterns(supportedFiles).join(',')}}`; + const watcher = workspace.createFileSystemWatcher(globPattern); + + const updateFiles = (filePath: string): void => { + extension.snykCodeOld.addChangedFile(filePath); + void extension.runCodeScan(); // It's debounced, so not worries about concurrent calls + }; + + watcher.onDidChange((documentUri: vscode.Uri) => { + updateFiles(documentUri.fsPath); + }); + watcher.onDidDelete((documentUri: vscode.Uri) => { + updateFiles(documentUri.fsPath); + }); + watcher.onDidCreate((documentUri: vscode.Uri) => { + updateFiles(documentUri.fsPath); + }); + + return watcher; +} diff --git a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts b/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts index bbd56aa64..8fc56f11d 100644 --- a/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts +++ b/src/snyk/snykOss/hoverProvider/vulnerabilityCountHoverProvider.ts @@ -2,15 +2,8 @@ import { IAnalytics } from '../../common/analytics/itly'; import { IDE_NAME } from '../../common/constants/general'; import { SupportedLanguageIds } from '../../common/constants/languageConsts'; import { IVSCodeLanguages } from '../../common/vscode/languages'; -import { - Diagnostic, - DiagnosticCollection, - Disposable, - Hover, - Position, - Range, - TextDocument, -} from '../../common/vscode/types'; +import { DiagnosticCollection, Disposable, Hover, Position, TextDocument } from '../../common/vscode/types'; +import { IssueUtils } from '../../snykCode/utils/issueUtils'; export class VulnerabilityCountHoverProvider implements Disposable { private hoverProvider: Disposable | undefined; @@ -34,7 +27,7 @@ export class VulnerabilityCountHoverProvider implements Disposable { } const currentFileReviewIssues = diagnostics.get(document.uri); - const issue = VulnerabilityCountHoverProvider.findIssueWithRange(position, currentFileReviewIssues); + const issue = IssueUtils.findIssueWithRange(position, currentFileReviewIssues); if (issue) { this.logIssueHoverIsDisplayed(); } @@ -48,18 +41,6 @@ export class VulnerabilityCountHoverProvider implements Disposable { }); } - private static findIssueWithRange = ( - matchingRange: Range | Position, - issuesList: readonly Diagnostic[] | undefined, - ): Diagnostic | undefined => { - return ( - issuesList && - issuesList.find((issue: Diagnostic) => { - return issue.range.contains(matchingRange); - }) - ); - }; - dispose(): void { if (this.hoverProvider) { this.hoverProvider.dispose(); diff --git a/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts index 5f1eafe27..34daadf40 100644 --- a/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts +++ b/src/snyk/snykOss/views/ossVulnerabilityTreeProvider.ts @@ -1,7 +1,7 @@ import { OpenCommandIssueType, OpenIssueCommandArg } from '../../common/commands/types'; import { IConfiguration } from '../../common/configuration/configuration'; import { SNYK_OPEN_ISSUE_COMMAND } from '../../common/constants/commands'; -import { SNYK_SCAN_STATUS } from '../../common/constants/views'; +import { SNYK_ANALYSIS_STATUS } from '../../common/constants/views'; import { messages as commonMessages } from '../../common/messages/analysisMessages'; import { IContextService } from '../../common/services/contextService'; import { IViewManagerService } from '../../common/services/viewManagerService'; @@ -34,7 +34,7 @@ export class OssVulnerabilityTreeProvider extends AnalysisTreeNodeProvder { if (!this.configuration.getFeaturesConfiguration()?.ossEnabled) { return [ new TreeNode({ - text: SNYK_SCAN_STATUS.OSS_DISABLED, + text: SNYK_ANALYSIS_STATUS.OSS_DISABLED, }), ]; } diff --git a/src/test/unit/base/services/authenticationService.test.ts b/src/test/unit/base/services/authenticationService.test.ts index 09abb35a8..150c6f2a5 100644 --- a/src/test/unit/base/services/authenticationService.test.ts +++ b/src/test/unit/base/services/authenticationService.test.ts @@ -1,4 +1,6 @@ +import { getIpFamily } from '@snyk/code-client'; import { rejects, strictEqual } from 'assert'; +import needle, { NeedleResponse } from 'needle'; import sinon from 'sinon'; import { IBaseSnykModule } from '../../../../snyk/base/modules/interfaces'; import { AuthenticationService } from '../../../../snyk/base/services/authenticationService'; @@ -8,7 +10,6 @@ import { IConfiguration } from '../../../../snyk/common/configuration/configurat import { DID_CHANGE_CONFIGURATION_METHOD } from '../../../../snyk/common/constants/languageServer'; import { SNYK_CONTEXT } from '../../../../snyk/common/constants/views'; import { IContextService } from '../../../../snyk/common/services/contextService'; -import { IVSCodeCommands } from '../../../../snyk/common/vscode/commands'; import { ILanguageClientAdapter } from '../../../../snyk/common/vscode/languageClient'; import { LanguageClient } from '../../../../snyk/common/vscode/types'; import { LoggerMock } from '../../mocks/logger.mock'; @@ -25,7 +26,16 @@ suite('AuthenticationService', () => { let clearTokenSpy: sinon.SinonSpy; let previewFeaturesSpy: sinon.SinonSpy; - let commands: IVSCodeCommands; + const NEEDLE_DEFAULT_TIMEOUT = 1000; + + const overrideNeedleTimeoutOptions = { + // eslint-disable-next-line camelcase + open_timeout: NEEDLE_DEFAULT_TIMEOUT, + // eslint-disable-next-line camelcase + response_timeout: NEEDLE_DEFAULT_TIMEOUT, + // eslint-disable-next-line camelcase + read_timeout: NEEDLE_DEFAULT_TIMEOUT, + }; setup(() => { baseModule = {} as IBaseSnykModule; @@ -53,10 +63,6 @@ suite('AuthenticationService', () => { clearToken: clearTokenSpy, getPreviewFeatures: previewFeaturesSpy, } as unknown as IConfiguration; - - commands = { - executeCommand: sinon.fake(), - }; }); teardown(() => sinon.restore()); @@ -74,7 +80,6 @@ suite('AuthenticationService', () => { analytics, new LoggerMock(), languageClientAdapter, - commands, ); await service.initiateLogin(); @@ -82,6 +87,52 @@ suite('AuthenticationService', () => { strictEqual(logAuthenticateButtonIsClickedFake.calledOnce, true); }); + // TODO: the following two tests are more of integration tests, since the second requires access to the network layer. Move it to integration test as part of ROAD-625. + test('getIpFamily returns undefined when IPv6 not supported', async () => { + const ipv6ErrorCode = 'EADDRNOTAVAIL'; + + // code-client calls 'needle', thus it's the easiest place to stub the response when IPv6 is not supported by the OS network stack. Otherwise, Node internals must be stubbed to return the error. + sinon.stub(needle, 'request').callsFake((_, uri, data, opts, callback) => { + if (!callback) throw new Error(); + callback( + { + code: ipv6ErrorCode, + errno: ipv6ErrorCode, + } as unknown as Error, + {} as unknown as NeedleResponse, + null, + ); + // eslint-disable-next-line camelcase + return needle.post(uri, data, { ...opts, ...overrideNeedleTimeoutOptions }); + }); + + const ipFamily = await getIpFamily('https://dev.snyk.io'); + + strictEqual(ipFamily, undefined); + }); + + test('getIpFamily returns 6 when IPv6 supported', async () => { + sinon.stub(needle, 'request').callsFake((_, uri, data, opts, callback) => { + if (!callback) throw new Error(); + callback( + null, + { + body: { + response: { + statusCode: 401, + body: {}, + }, + }, + } as NeedleResponse, + null, + ); + return needle.post(uri, data, { ...opts, ...overrideNeedleTimeoutOptions }); + }); + + const ipFamily = await getIpFamily('https://dev.snyk.io'); + strictEqual(ipFamily, 6); + }); + test("Doesn't call setToken when token is empty", async () => { const service = new AuthenticationService( contextService, @@ -91,7 +142,6 @@ suite('AuthenticationService', () => { {} as IAnalytics, new LoggerMock(), languageClientAdapter, - commands, ); sinon.replace(windowMock, 'showInputBox', sinon.fake.returns('')); @@ -110,7 +160,6 @@ suite('AuthenticationService', () => { {} as IAnalytics, new LoggerMock(), languageClientAdapter, - commands, ); const tokenValue = 'token-value'; sinon.replace(windowMock, 'showInputBox', sinon.fake.returns(tokenValue)); @@ -140,7 +189,6 @@ suite('AuthenticationService', () => { {} as IAnalytics, new LoggerMock(), languageClientAdapter, - commands, ); }); diff --git a/src/test/unit/common/commands/commandController.test.ts b/src/test/unit/common/commands/commandController.test.ts index 85d224a0e..3c6c1fc9b 100644 --- a/src/test/unit/common/commands/commandController.test.ts +++ b/src/test/unit/common/commands/commandController.test.ts @@ -1,12 +1,15 @@ import sinon from 'sinon'; import * as util from 'util'; import { IAuthenticationService } from '../../../../snyk/base/services/authenticationService'; +import { ScanModeService } from '../../../../snyk/base/services/scanModeService'; import { IAnalytics } from '../../../../snyk/common/analytics/itly'; import { CommandController } from '../../../../snyk/common/commands/commandController'; import { COMMAND_DEBOUNCE_INTERVAL } from '../../../../snyk/common/constants/general'; +import { IOpenerService } from '../../../../snyk/common/services/openerService'; import { IVSCodeCommands } from '../../../../snyk/common/vscode/commands'; import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; import { ISnykCodeService } from '../../../../snyk/snykCode/codeService'; +import { ISnykCodeServiceOld } from '../../../../snyk/snykCode/codeServiceOld'; import { OssService } from '../../../../snyk/snykOss/services/ossService'; import { LanguageServerMock } from '../../mocks/languageServer.mock'; import { LoggerMock } from '../../mocks/logger.mock'; @@ -19,9 +22,12 @@ suite('CommandController', () => { setup(() => { controller = new CommandController( + {} as IOpenerService, {} as IAuthenticationService, {} as ISnykCodeService, + {} as ISnykCodeServiceOld, {} as OssService, + {} as ScanModeService, {} as IVSCodeWorkspace, {} as IVSCodeCommands, windowMock, diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index e8339af35..2e6a4cd26 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -184,12 +184,16 @@ suite('Configuration', () => { const configuration = new Configuration({}, workspace); deepStrictEqual(configuration.getPreviewFeatures(), { + lsCode: false, + reportFalsePositives: false, advisor: false, } as PreviewFeatures); }); test('Preview features: some features enabled', () => { const previewFeatures = { + reportFalsePositives: true, + lsCode: false, advisor: false, } as PreviewFeatures; const workspace = stubWorkspaceConfiguration(FEATURES_PREVIEW_SETTING, previewFeatures); diff --git a/src/test/unit/common/languageServer/languageServer.test.ts b/src/test/unit/common/languageServer/languageServer.test.ts index 99035562d..719799b6f 100644 --- a/src/test/unit/common/languageServer/languageServer.test.ts +++ b/src/test/unit/common/languageServer/languageServer.test.ts @@ -49,6 +49,7 @@ suite('Language Server', () => { getPreviewFeatures() { return { advisor: false, + reportFalsePositives: false, }; }, getFeaturesConfiguration() { diff --git a/src/test/unit/common/languageServer/middleware.test.ts b/src/test/unit/common/languageServer/middleware.test.ts index dfefc639b..80c36a2b3 100644 --- a/src/test/unit/common/languageServer/middleware.test.ts +++ b/src/test/unit/common/languageServer/middleware.test.ts @@ -31,6 +31,7 @@ suite('Language Server: Middleware', () => { getPreviewFeatures: () => { return { advisor: false, + reportFalsePositives: false, }; }, getFeaturesConfiguration() { diff --git a/src/test/unit/common/services/cliConfigService.test.ts b/src/test/unit/common/services/cliConfigService.test.ts new file mode 100644 index 000000000..bd339c109 --- /dev/null +++ b/src/test/unit/common/services/cliConfigService.test.ts @@ -0,0 +1,45 @@ +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { ISnykApiClient } from '../../../../snyk/common/api/apiСlient'; +import { Configuration, IConfiguration } from '../../../../snyk/common/configuration/configuration'; +import { getSastSettings } from '../../../../snyk/common/services/cliConfigService'; + +suite('CLI Config Service', () => { + let config: IConfiguration; + + setup(() => { + config = { + organization: 'my-super-org', + } as IConfiguration; + }); + + teardown(() => { + sinon.restore(); + }); + + test('IDE header and URL query param are passed to settings endpoint', async () => { + // arrange + const getFake = sinon.stub().returns({ + data: {}, + }); + const apiClient: ISnykApiClient = { + get: getFake, + }; + + // act + await getSastSettings(apiClient, config); + + // assert + strictEqual( + getFake.calledWith(sinon.match.any, { + headers: { + 'x-snyk-ide': `${Configuration.source}-${await Configuration.getVersion()}`, + }, + params: { + org: config.organization, + }, + }), + true, + ); + }); +}); diff --git a/src/test/unit/common/services/learnService.test.ts b/src/test/unit/common/services/learnService.test.ts index f4e43f020..dc663d1ef 100644 --- a/src/test/unit/common/services/learnService.test.ts +++ b/src/test/unit/common/services/learnService.test.ts @@ -5,6 +5,7 @@ import { OpenCommandIssueType } from '../../../../snyk/common/commands/types'; import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; import { CodeIssueData, Issue } from '../../../../snyk/common/languageServer/types'; import { LearnService } from '../../../../snyk/common/services/learnService'; +import type { completeFileSuggestionType } from '../../../../snyk/snykCode/interfaces'; import { OssIssueCommandArg } from '../../../../snyk/snykOss/views/ossVulnerabilityTreeProvider'; import { LoggerMock } from '../../mocks/logger.mock'; @@ -18,6 +19,12 @@ suite('LearnService', () => { }, } as Issue; + const codeIssueCommandArgFixtureOld = { + isSecurityType: true, + cwe: ['CWE-2'], + id: 'javascript%2Fdc_interfile_project%2FSqli', + } as completeFileSuggestionType; + const lessonFixture = { title: 'lesson title', lessonId: 'id', @@ -84,6 +91,14 @@ suite('LearnService', () => { }); }); + test('getCodeIssueParams - returns ecosystem & cwes - DEPRECATED', () => { + deepStrictEqual(LearnService.getCodeIssueParamsOld(codeIssueCommandArgFixtureOld), { + ecosystem: 'javascript', + rule: 'Sqli', + cwes: ['CWE-2'], + }); + }); + test('getLesson - resolves a lesson', async () => { const stub = sinon.stub(axios, 'get').resolves({ data: { lessons: [lessonFixture] } }); const lesson = await learnService.getLesson(codeIssueCommandArgFixture, OpenCommandIssueType.CodeIssue); @@ -103,6 +118,25 @@ suite('LearnService', () => { ]); }); + test('getLesson - resolves a lesson - DEPRECATED', async () => { + const stub = sinon.stub(axios, 'get').resolves({ data: { lessons: [lessonFixture] } }); + const lesson = await learnService.getLesson(codeIssueCommandArgFixtureOld, OpenCommandIssueType.CodeIssueOld); + deepStrictEqual(lesson?.lessonId, lessonFixture.lessonId); + deepStrictEqual(stub.getCall(0).args, [ + '/lessons/lookup-for-cta', + { + baseURL: `${config.baseApiUrl}/v1/learn`, + params: { + source: 'ide', + cwe: codeIssueCommandArgFixtureOld.cwe[0], + rule: 'Sqli', + ecosystem: 'javascript', + cve: undefined, + }, + }, + ]); + }); + test('getLesson - returns null if issue isSecurityType is false', async () => { sinon.stub(axios, 'get').resolves({ data: { lessons: [lessonFixture] } }); const lesson = await learnService.getLesson( @@ -118,10 +152,7 @@ suite('LearnService', () => { test('getLesson - returns null if issue isSecurityType is false', async () => { sinon.stub(axios, 'get').resolves({ data: { lessons: [lessonFixture] } }); const lesson = await learnService.getLesson( - { - ...codeIssueCommandArgFixture, - additionalData: { ...codeIssueCommandArgFixture.additionalData, isSecurityType: false }, - }, + { ...codeIssueCommandArgFixtureOld, isSecurityType: false }, OpenCommandIssueType.CodeIssueOld, ); deepStrictEqual(lesson, null); @@ -160,6 +191,19 @@ suite('LearnService', () => { deepStrictEqual(lessonNoEcosystem, null); }); + test('returns null if the issue has no params - DEPRECATED', async () => { + const lessonNoCWE = await learnService.getLesson( + { ...codeIssueCommandArgFixtureOld, cwe: [] }, + OpenCommandIssueType.CodeIssueOld, + ); + deepStrictEqual(lessonNoCWE, null); + const lessonNoEcosystem = await learnService.getLesson( + { ...codeIssueCommandArgFixtureOld, id: '' }, + OpenCommandIssueType.CodeIssueOld, + ); + deepStrictEqual(lessonNoEcosystem, null); + }); + test('returns null if no lessons are returned', async () => { sinon.stub(axios, 'get').resolves({ data: { lessons: [] } }); const lesson = await learnService.getLesson(ossIssueCommandArgFixture, OpenCommandIssueType.OssVulnerability); diff --git a/src/test/unit/snykCode/analyzer/analyzer.test.ts b/src/test/unit/snykCode/analyzer/analyzer.test.ts new file mode 100644 index 000000000..ae06050af --- /dev/null +++ b/src/test/unit/snykCode/analyzer/analyzer.test.ts @@ -0,0 +1,89 @@ +import { AnalysisSeverity } from '@snyk/code-client'; +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { Configuration } from '../../../../snyk/common/configuration/configuration'; +import { + CODE_QUALITY_ENABLED_SETTING, + CODE_SECURITY_ENABLED_SETTING, + SEVERITY_FILTER_SETTING, +} from '../../../../snyk/common/constants/settings'; +import SnykCodeAnalyzer from '../../../../snyk/snykCode/analyzer/analyzer'; +import { stubWorkspaceConfiguration } from '../../mocks/workspace.mock'; + +suite('Snyk Code Analyzer', () => { + teardown(() => { + sinon.restore(); + }); + + test('Security Issue is visible if Code Security is enabled', () => { + const securityIssue = true; + const workspace = stubWorkspaceConfiguration(CODE_SECURITY_ENABLED_SETTING, true); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, securityIssue, AnalysisSeverity.critical), true); + }); + + test('Security Issue is not visible if Code Security is disabled', () => { + const securityIssue = true; + const workspace = stubWorkspaceConfiguration(CODE_SECURITY_ENABLED_SETTING, false); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, securityIssue, AnalysisSeverity.critical), false); + }); + + test('Quality Issue is visible if Code Quality is enabled', () => { + const securityIssue = false; + const workspace = stubWorkspaceConfiguration(CODE_QUALITY_ENABLED_SETTING, true); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, securityIssue, AnalysisSeverity.critical), true); + }); + + test('Quality Issue is not visible if Code Quality is disabled', () => { + const securityIssue = false; + const workspace = stubWorkspaceConfiguration(CODE_QUALITY_ENABLED_SETTING, false); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, securityIssue, AnalysisSeverity.critical), false); + }); + + test('Critical severity issue respects high severity filter', () => { + const filter = { + critical: false, + high: false, + }; + const workspace = stubWorkspaceConfiguration(SEVERITY_FILTER_SETTING, filter); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.critical), false); + + filter.high = true; + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.critical), true); + }); + + test('Warning severity issue respects medium severity filter', () => { + const filter = { + medium: false, + }; + const workspace = stubWorkspaceConfiguration(SEVERITY_FILTER_SETTING, filter); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.warning), false); + + filter.medium = true; + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.warning), true); + }); + + test('Info severity issue is not visible if low severity is disabled', () => { + const filter = { + low: false, + }; + const workspace = stubWorkspaceConfiguration(SEVERITY_FILTER_SETTING, filter); + const config = new Configuration({}, workspace); + + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.info), false); + + filter.low = true; + strictEqual(SnykCodeAnalyzer.isIssueVisible(config, true, AnalysisSeverity.info), true); + }); +}); diff --git a/src/test/unit/snykCode/codeActions/issuesActionsProvider.test.ts b/src/test/unit/snykCode/codeActions/issuesActionsProvider.test.ts new file mode 100644 index 000000000..5e748a052 --- /dev/null +++ b/src/test/unit/snykCode/codeActions/issuesActionsProvider.test.ts @@ -0,0 +1,71 @@ +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { IAnalytics } from '../../../../snyk/common/analytics/itly'; +import { ICodeActionAdapter, ICodeActionKindAdapter } from '../../../../snyk/common/vscode/codeAction'; +import { + CodeActionKind, + Diagnostic, + DiagnosticCollection, + Range, + TextDocument, + Uri, +} from '../../../../snyk/common/vscode/types'; +import { SnykIssuesActionProviderOld } from '../../../../snyk/snykCode/codeActions/issuesActionsProviderOld'; +import { IssueUtils } from '../../../../snyk/snykCode/utils/issueUtils'; + +suite('Snyk Code actions provider (OLD)', () => { + let issuesActionsProvider: SnykIssuesActionProviderOld; + const logQuickFixIsDisplayed = sinon.fake(); + + setup(() => { + const snykReview = { + has: (_: Uri): boolean => true, + get: sinon.fake(), + } as unknown as DiagnosticCollection; + + const analytics = { + logQuickFixIsDisplayed, + } as unknown as IAnalytics; + + const fakeCodeAction = { + command: {}, + }; + + const codeActionAdapter = { + create: (_: string, _kind?: CodeActionKind) => fakeCodeAction, + } as ICodeActionAdapter; + const codeActionKindAdapter = { + getQuickFix: sinon.fake(), + } as ICodeActionKindAdapter; + + const callbacks = { + findSuggestion: () => true, + }; + + issuesActionsProvider = new SnykIssuesActionProviderOld( + snykReview, + callbacks, + codeActionAdapter, + codeActionKindAdapter, + analytics, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test("Logs 'Quick Fix is Displayed' analytical event", () => { + // prepare objects + sinon.stub(IssueUtils, 'findIssueWithRange').returns({} as Diagnostic); + const document = { + uri: 'test.js', + } as unknown as TextDocument; + + // act + issuesActionsProvider.provideCodeActions(document, {} as Range); + + // verify + strictEqual(logQuickFixIsDisplayed.calledOnce, true); + }); +}); diff --git a/src/test/unit/snykCode/codeSettings.test.ts b/src/test/unit/snykCode/codeSettings.test.ts new file mode 100644 index 000000000..50b8d6c51 --- /dev/null +++ b/src/test/unit/snykCode/codeSettings.test.ts @@ -0,0 +1,97 @@ +import { strictEqual } from 'assert'; +import sinon, { SinonSpy } from 'sinon'; +import { ISnykApiClient } from '../../../snyk/common/api/apiСlient'; +import { IConfiguration } from '../../../snyk/common/configuration/configuration'; +import { SNYK_CONTEXT } from '../../../snyk/common/constants/views'; +import { IContextService } from '../../../snyk/common/services/contextService'; +import { IOpenerService } from '../../../snyk/common/services/openerService'; +import { CodeSettings, ICodeSettings } from '../../../snyk/snykCode/codeSettings'; + +suite('Snyk Code Settings', () => { + let settings: ICodeSettings; + let setContextFake: SinonSpy; + let contextService: IContextService; + + setup(() => { + setContextFake = sinon.fake(); + + contextService = { + setContext: setContextFake, + shouldShowCodeAnalysis: false, + shouldShowOssAnalysis: false, + viewContext: {}, + }; + + settings = new CodeSettings({} as ISnykApiClient, contextService, {} as IConfiguration, {} as IOpenerService); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Code is disabled when SAST is disabled', async () => { + sinon.stub(CodeSettings.prototype, 'getSastSettings').resolves({ + sastEnabled: false, + localCodeEngine: { + enabled: false, + }, + reportFalsePositivesEnabled: true, + }); + + const codeEnabled = await settings.checkCodeEnabled(); + + strictEqual(codeEnabled, false); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_ENABLED, false), true); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_LOCAL_ENGINE_ENABLED, false), true); + }); + + test('Code is enabled when SAST is enabled and LCE is disabled', async () => { + sinon.stub(CodeSettings.prototype, 'getSastSettings').resolves({ + sastEnabled: true, + localCodeEngine: { + enabled: false, + }, + reportFalsePositivesEnabled: true, + }); + + const codeEnabled = await settings.checkCodeEnabled(); + + strictEqual(codeEnabled, true); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_ENABLED, true), true); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_LOCAL_ENGINE_ENABLED, false), true); + }); + + test('Code is disabled when LCE is enabled', async () => { + sinon.stub(CodeSettings.prototype, 'getSastSettings').resolves({ + sastEnabled: true, + localCodeEngine: { + enabled: true, + }, + reportFalsePositivesEnabled: true, + }); + + const codeEnabled = await settings.checkCodeEnabled(); + + strictEqual(codeEnabled, false); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_ENABLED, true), true); + strictEqual(setContextFake.calledWith(SNYK_CONTEXT.CODE_LOCAL_ENGINE_ENABLED, true), true); + }); + + test('Entitlement reportFalsePositivesEnabled gets cached', async () => { + const getFake = sinon.stub().returns({ + data: { + reportFalsePositivesEnabled: true, + }, + }); + + const apiClient: ISnykApiClient = { + get: getFake, + }; + + settings = new CodeSettings(apiClient, contextService, {} as IConfiguration, {} as IOpenerService); + + await settings.getSastSettings(); + + strictEqual(settings.reportFalsePositivesEnabled, true); + }); +}); diff --git a/src/test/unit/snykCode/error/snykCodeErrorHandler.test.ts b/src/test/unit/snykCode/error/snykCodeErrorHandler.test.ts new file mode 100644 index 000000000..ae80251fb --- /dev/null +++ b/src/test/unit/snykCode/error/snykCodeErrorHandler.test.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { constants } from '@snyk/code-client'; +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { IBaseSnykModule } from '../../../../snyk/base/modules/interfaces'; +import { ILoadingBadge } from '../../../../snyk/base/views/loadingBadge'; +import { IConfiguration } from '../../../../snyk/common/configuration/configuration'; +import { CONNECTION_ERROR_RETRY_INTERVAL, MAX_CONNECTION_RETRIES } from '../../../../snyk/common/constants/general'; +import { IContextService } from '../../../../snyk/common/services/contextService'; +import { SnykCodeErrorHandler } from '../../../../snyk/snykCode/error/snykCodeErrorHandler'; +import { LoggerMock } from '../../mocks/logger.mock'; + +suite('Snyk Code Error Handler', () => { + const runCodeScanFake = sinon.stub().resolves(); + const baseSnykModule = { + runCodeScan: runCodeScanFake, + } as unknown as IBaseSnykModule; + + const handler = new SnykCodeErrorHandler( + {} as IContextService, + {} as ILoadingBadge, + new LoggerMock(), + baseSnykModule, + {} as IConfiguration, + ); + + teardown(() => { + sinon.restore(); + }); + + test('Retries scan if "Failed to get remote bundle" is processed', async function () { + // arrange + this.timeout(CONNECTION_ERROR_RETRY_INTERVAL + 2000); + + const error = new Error('Failed to get remote bundle'); + // act + await handler.processError(error, undefined, '123456789', () => null); + + strictEqual(handler.connectionRetryLimitExhausted, false); + // assert + return new Promise((resolve, _) => { + setTimeout(() => { + strictEqual(runCodeScanFake.called, true); + resolve(); + }, CONNECTION_ERROR_RETRY_INTERVAL); + }); + }); + + test('Handles Snyk Code api error response and retries appropriately', async function () { + this.timeout(CONNECTION_ERROR_RETRY_INTERVAL + 2000); + const error = { + apiName: 'getAnalysis', + messages: { + 500: 'Unexpected server error', + }, + errorCode: 500, + }; + + await handler.processError(error, undefined, '123456789', () => null); + + strictEqual(handler.connectionRetryLimitExhausted, false); + // assert + return new Promise((resolve, _) => { + setTimeout(() => { + strictEqual(runCodeScanFake.called, true); + resolve(); + }, CONNECTION_ERROR_RETRY_INTERVAL); + }); + }); + + test('Logs analytic events once retries are exhausted', async function () { + this.timeout(CONNECTION_ERROR_RETRY_INTERVAL + 2000); + const error = new Error('Failed to get remote bundle'); + + const mockedRetries = []; + // parallelising the retries + for (let i = 0; i < MAX_CONNECTION_RETRIES + 2; i++) { + mockedRetries.push(handler.processError(error, undefined, '123456789', () => null)); + } + await Promise.all(mockedRetries); + + strictEqual(handler.connectionRetryLimitExhausted, true); + }); + + test('404 is retryable error', function () { + strictEqual(SnykCodeErrorHandler.isErrorRetryable(constants.ErrorCodes.notFound), true); + }); +}); diff --git a/src/test/unit/snykCode/falsePositive/falsePositive.test.ts b/src/test/unit/snykCode/falsePositive/falsePositive.test.ts new file mode 100644 index 000000000..4f9a5427b --- /dev/null +++ b/src/test/unit/snykCode/falsePositive/falsePositive.test.ts @@ -0,0 +1,68 @@ +import { Marker } from '@snyk/code-client'; +import { strictEqual, throws } from 'assert'; +import * as os from 'os'; +import sinon from 'sinon'; +import { TextDocument } from '../../../../snyk/common/vscode/types'; +import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; +import { FalsePositive } from '../../../../snyk/snykCode/falsePositive/falsePositive'; +import { completeFileSuggestionType } from '../../../../snyk/snykCode/interfaces'; + +suite('False Positive', () => { + let workspace: IVSCodeWorkspace; + + setup(() => { + workspace = {} as IVSCodeWorkspace; + }); + + teardown(() => { + sinon.restore(); + }); + + test('Instantiation throws when markers are empty', () => { + throws(() => new FalsePositive(workspace, {} as completeFileSuggestionType)); + }); + + test('Returns correct generated content', async () => { + // arrange + const filePath = os.platform() === 'win32' ? 'C:\\Users\\snyk\\goof\\test.js' : '/Users/snyk/goof/test.js'; + const marker = { + msg: [0, 1], + pos: [{ file: filePath, cols: [10, 20], rows: [1, 1] }], + } as Marker; + const suggestion = { + cols: [10, 20], + rows: [1, 1], + markers: [marker], + } as completeFileSuggestionType; + + const workspaceFolder = os.platform() === 'win32' ? 'C:\\Users\\snyk\\goof' : '/Users/snyk/goof'; + const text = 'console.log("Hello world");'; + const textDocument = { + getText: () => text, + } as unknown as TextDocument; + + workspace = { + getWorkspaceFolders: () => [workspaceFolder], + openFileTextDocument: () => Promise.resolve(textDocument), + } as unknown as IVSCodeWorkspace; + + const fp = new FalsePositive(workspace, suggestion); + + // act + const content = await fp.getGeneratedContent(); + + // assert + const expectedContent = ` +/** + * The following code will be uploaded to Snyk to be reviewed. + * Make sure there are no sensitive information sent. + */ + +/** + * Code from ${filePath} + */ +${text}`.trimStart(); + + strictEqual(content, expectedContent); + }); +}); diff --git a/src/test/unit/snykCode/hoverProvider/hoverProvider.test.ts b/src/test/unit/snykCode/hoverProvider/hoverProvider.test.ts new file mode 100644 index 000000000..bd212563c --- /dev/null +++ b/src/test/unit/snykCode/hoverProvider/hoverProvider.test.ts @@ -0,0 +1,59 @@ +import { strictEqual } from 'assert'; +import sinon from 'sinon'; +import { IAnalytics } from '../../../../snyk/common/analytics/itly'; +import { IHoverAdapter } from '../../../../snyk/common/vscode/hover'; +import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; +import { IMarkdownStringAdapter } from '../../../../snyk/common/vscode/markdownString'; +import { Diagnostic, DiagnosticCollection, Position, TextDocument, Uri } from '../../../../snyk/common/vscode/types'; +import { DisposableHoverProvider } from '../../../../snyk/snykCode/hoverProvider/hoverProvider'; +import { ISnykCodeAnalyzer } from '../../../../snyk/snykCode/interfaces'; +import { IssueUtils } from '../../../../snyk/snykCode/utils/issueUtils'; +import { LoggerMock } from '../../mocks/logger.mock'; + +suite('Snyk Code hover provider', () => { + let provider: DisposableHoverProvider; + const logIssueHoverIsDisplayed = sinon.fake(); + + setup(() => { + const analyzer = { + findSuggestion: (_: string) => true, + } as unknown as ISnykCodeAnalyzer; + const vscodeLanguagesMock = sinon.fake() as unknown as IVSCodeLanguages; + + const analytics = { + logIssueHoverIsDisplayed, + } as unknown as IAnalytics; + + provider = new DisposableHoverProvider(analyzer, new LoggerMock(), vscodeLanguagesMock, analytics, { + get: sinon.fake(), + } as IMarkdownStringAdapter); + }); + + teardown(() => { + sinon.restore(); + }); + + test("Logs 'Issue Hover is Displayed' analytical event", () => { + // prepare objects + const snykReview = { + has: (_: Uri): boolean => true, + get: sinon.fake(), + } as unknown as DiagnosticCollection; + + sinon.stub(IssueUtils, 'findIssueWithRange').returns({} as Diagnostic); + + const hoverProvider = provider.getHover(snykReview, { + create: sinon.fake() as unknown, + } as IHoverAdapter); + + const document = { + uri: 'test.js', + } as unknown as TextDocument; + + // act + hoverProvider(document, {} as Position); + + // verify + strictEqual(logIssueHoverIsDisplayed.calledOnce, true); + }); +}); diff --git a/src/test/unit/snykCode/utils/analysisUtils.test.ts b/src/test/unit/snykCode/utils/analysisUtils.test.ts index 0baa75654..d7755d2bb 100644 --- a/src/test/unit/snykCode/utils/analysisUtils.test.ts +++ b/src/test/unit/snykCode/utils/analysisUtils.test.ts @@ -1,19 +1,67 @@ -import { strictEqual } from 'assert'; +import { Marker } from '@snyk/code-client'; +import { deepStrictEqual, strictEqual } from 'assert'; import path from 'path'; import sinon from 'sinon'; +import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages'; import { IVSCodeWorkspace } from '../../../../snyk/common/vscode/workspace'; -import { getAbsoluteMarkerFilePath } from '../../../../snyk/snykCode/utils/analysisUtils'; +import { + createIssueRange, + createIssueRelatedInformation, + getAbsoluteMarkerFilePath, +} from '../../../../snyk/snykCode/utils/analysisUtils'; +import { IssuePlacementPosition } from '../../../../snyk/snykCode/utils/issueUtils'; +import { languagesMock } from '../../mocks/languages.mock'; +import { uriAdapterMock } from '../../mocks/uri.mock'; +import { workspaceFolder, workspaceMock } from '../../mocks/workspace.mock'; suite('Snyk Code Analysis Utils', () => { const createRangeMock = sinon.mock(); + let languages: IVSCodeLanguages; let workspace: IVSCodeWorkspace; setup(() => { + languages = { + createRange: createRangeMock, + } as unknown as IVSCodeLanguages; workspace = {} as IVSCodeWorkspace; }); teardown(() => createRangeMock.reset()); + test('Create issue range copes with non-negative values', () => { + const position: IssuePlacementPosition = { + cols: { + start: 1, + end: 1, + }, + rows: { + start: 1, + end: 1, + }, + }; + + createIssueRange(position, languages); + + sinon.assert.calledOnceWithExactly(createRangeMock, 1, 1, 1, 1); + }); + + test('Create issue range copes with negative values', () => { + const position: IssuePlacementPosition = { + cols: { + start: -1, + end: -1, + }, + rows: { + start: -1, + end: -1, + }, + }; + + createIssueRange(position, languages); + + sinon.assert.calledOnceWithExactly(createRangeMock, 0, 0, 0, 0); + }); + test('Returns correct absolute path if no marker file path provided', () => { // arrange const suggestionFilePath = '/Users/snyk/goof/test.js'; @@ -56,4 +104,56 @@ suite('Snyk Code Analysis Utils', () => { // assert strictEqual(absoluteFilePath, path.resolve(workspaceFolder, relativeMarkerFilePath)); }); + + test('Creates correct related information for inter-file issues', () => { + const file1Uri = 'file1.js'; + const file2Uri = 'file2.js'; + + const markers: Marker[] = [ + { + msg: [0, 16], + pos: [ + { + file: file1Uri, + rows: [1, 1], + cols: [1, 1], + }, + { + file: file2Uri, + rows: [2, 2], + cols: [10, 10], + }, + ], + }, + ]; + + const message = + 'Unsanitized input from data from a remote resource flows into bypassSecurityTrustHtml, where it is used to render an HTML page returned to the user. This may result in a Cross-Site Scripting attack (XSS).'; + + const information = createIssueRelatedInformation( + markers, + file2Uri, + message, + languagesMock, + workspaceMock, + uriAdapterMock, + ); + + deepStrictEqual(information, [ + { + message: 'Unsanitized input', + location: { + uri: { path: path.join(workspaceFolder, file1Uri) }, + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, + }, + }, + { + message: 'Unsanitized input', + location: { + uri: { path: path.join(workspaceFolder, file2Uri) }, + range: { start: { line: 1, character: 9 }, end: { line: 1, character: 10 } }, + }, + }, + ]); + }); });