From 6e3d3d8de34d9090c46ed62b02927407fe9bbeb6 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Wed, 1 Nov 2023 13:24:56 +0200 Subject: [PATCH 1/6] refactor: single-source to multi-source Signed-off-by: Ilona Shishov --- package-lock.json | 256 ++++++++++++--------------- package.json | 1 - src/aggregators.ts | 56 ------ src/codeActionHandler.ts | 52 ++++++ src/collector.ts | 5 +- src/componentAnalysis.ts | 112 ++++++++++++ src/config.ts | 24 ++- src/constants.ts | 14 ++ src/consumers.ts | 207 ---------------------- src/diagnosticsHandler.ts | 119 +++++++++++++ src/fileHandler.ts | 106 +++++++++++ src/providers/pom.xml.ts | 4 +- src/server.ts | 360 +++----------------------------------- src/utils.ts | 11 +- src/vulnerability.ts | 74 ++------ 15 files changed, 586 insertions(+), 815 deletions(-) delete mode 100644 src/aggregators.ts create mode 100644 src/codeActionHandler.ts create mode 100644 src/componentAnalysis.ts create mode 100644 src/constants.ts delete mode 100644 src/consumers.ts create mode 100644 src/diagnosticsHandler.ts create mode 100644 src/fileHandler.ts diff --git a/package-lock.json b/package-lock.json index 9cb08ecc..b0de97a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@types/chai": "^4.3.7", "@types/mocha": "^10.0.2", "@types/node": "^20.8.4", - "@types/node-fetch": "^2.6.6", "@types/uuid": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", @@ -36,6 +35,41 @@ "typescript": "^5.2.2" } }, + "../exhort-javascript-api": { + "name": "@RHEcosystemAppEng/exhort-javascript-api", + "version": "0.0.2-ea.49", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.23.2", + "@cyclonedx/cyclonedx-library": "^4.0.0", + "fast-xml-parser": "^4.2.4", + "packageurl-js": "^1.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "exhort-javascript-api": "dist/src/cli.js" + }, + "devDependencies": { + "@babel/core": "^7.23.2", + "@openapitools/openapi-generator-cli": "^2.6.0", + "@types/node": "^20.3.1", + "babel-plugin-rewire": "^1.2.0", + "c8": "^8.0.0", + "chai": "^4.3.7", + "eslint": "^8.42.0", + "eslint-plugin-editorconfig": "^4.0.3", + "mocha": "^10.2.0", + "msw": "^1.3.2", + "sinon": "^15.1.2", + "sinon-chai": "^3.7.0", + "typescript": "^5.1.3" + }, + "engines": { + "node": ">= 18.0.0", + "npm": ">= 9.0.0" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -400,9 +434,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -472,9 +506,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -863,24 +897,14 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-lX17GZVpJ/fuCjguZ5b3TjEbSENxmEk1B2z02yoXSK9WMEWRivhdSY73wWMn6bpcCDAOh6qAdktpKHIlkDk2lg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -894,16 +918,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", - "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", + "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.9.0", - "@typescript-eslint/type-utils": "6.9.0", - "@typescript-eslint/utils": "6.9.0", - "@typescript-eslint/visitor-keys": "6.9.0", + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/type-utils": "6.9.1", + "@typescript-eslint/utils": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -962,15 +986,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", - "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", + "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.9.0", - "@typescript-eslint/types": "6.9.0", - "@typescript-eslint/typescript-estree": "6.9.0", - "@typescript-eslint/visitor-keys": "6.9.0", + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", "debug": "^4.3.4" }, "engines": { @@ -990,13 +1014,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", - "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz", + "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.0", - "@typescript-eslint/visitor-keys": "6.9.0" + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1007,13 +1031,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", - "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz", + "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.9.0", - "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/utils": "6.9.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1034,9 +1058,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", - "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz", + "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1047,13 +1071,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", - "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz", + "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.0", - "@typescript-eslint/visitor-keys": "6.9.0", + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/visitor-keys": "6.9.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1107,17 +1131,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", - "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz", + "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.9.0", - "@typescript-eslint/types": "6.9.0", - "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/scope-manager": "6.9.1", + "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/typescript-estree": "6.9.1", "semver": "^7.5.4" }, "engines": { @@ -1165,12 +1189,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", - "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz", + "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/types": "6.9.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1436,12 +1460,6 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1559,9 +1577,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001558", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", - "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", "funding": [ { "type": "opencollective", @@ -1728,18 +1746,6 @@ "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -1847,15 +1853,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1911,9 +1908,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.569", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz", - "integrity": "sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg==" + "version": "1.4.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", + "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1943,15 +1940,15 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2251,9 +2248,9 @@ "devOptional": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2425,20 +2422,6 @@ "node": ">=8.0.0" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2871,9 +2854,9 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", + "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", "dev": true, "engines": { "node": ">=8" @@ -3317,27 +3300,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3973,9 +3935,9 @@ } }, "node_modules/packageurl-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.0.tgz", - "integrity": "sha512-JFoZnz1maKB0hTjn0YrmqRLgiU825SkbA370oe9ERcsKsj1EcBpe+CDo1EK9mrHc+18Hi5NmZbmXFQtP7YZEbw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz", + "integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -4137,9 +4099,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "devOptional": true, "engines": { "node": ">=6" diff --git a/package.json b/package.json index f104c269..dfa658ae 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@types/chai": "^4.3.7", "@types/mocha": "^10.0.2", "@types/node": "^20.8.4", - "@types/node-fetch": "^2.6.6", "@types/uuid": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", diff --git a/src/aggregators.ts b/src/aggregators.ts deleted file mode 100644 index bf0b8db9..00000000 --- a/src/aggregators.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat - * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; -import { IDependencyProvider } from './collector'; -import { Vulnerability } from './vulnerability'; - -/* VulnerabilityAggregator */ -interface VulnerabilityAggregator { - isNewVulnerability: boolean; - aggregate(newVulnerability: Vulnerability): Vulnerability; -} - -/* Noop Vulnerability aggregator class */ -class NoopVulnerabilityAggregator implements VulnerabilityAggregator { - isNewVulnerability: boolean; - provider: IDependencyProvider; - constructor(provider: IDependencyProvider) { - this.provider = provider; - } - - aggregate(newVulnerability: Vulnerability): Vulnerability { - // Make it a new vulnerability always and set ecosystem for vulnerability. - this.isNewVulnerability = true; - newVulnerability.provider = this.provider; - - return newVulnerability; - } -} - -/* Maven Vulnerability aggregator class */ -class MavenVulnerabilityAggregator implements VulnerabilityAggregator { - isNewVulnerability: boolean; - vulnerabilities: Map = new Map(); - provider: IDependencyProvider; - constructor(provider: IDependencyProvider) { - this.provider = provider; - } - - aggregate(newVulnerability: Vulnerability): Vulnerability { - // Make it a new vulnerability always and set ecosystem for vulnerability. - this.isNewVulnerability = true; - const key = `${newVulnerability.ref}@${newVulnerability.range.start.line}`; - const v = this.vulnerabilities.get(key); - if (v) { - this.isNewVulnerability = false; - return v; - } - newVulnerability.provider = this.provider; - this.vulnerabilities.set(key, newVulnerability); - return newVulnerability; - } -} - -export { VulnerabilityAggregator, NoopVulnerabilityAggregator, MavenVulnerabilityAggregator }; diff --git a/src/codeActionHandler.ts b/src/codeActionHandler.ts new file mode 100644 index 00000000..099f1a07 --- /dev/null +++ b/src/codeActionHandler.ts @@ -0,0 +1,52 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { CodeAction, CodeActionKind, Diagnostic } from 'vscode-languageserver/node'; +import { codeActionsMap } from './diagnosticsHandler'; +import { globalConfig } from './config'; +import { RHDA_DIAGNOSTIC_SOURCE } from './constants'; + +function getDiagnosticsCodeActions( diagnostics: Diagnostic[], fileType: string ): CodeAction[] { + const codeActions: CodeAction[] = []; + let hasRhdaDiagonostic: boolean = false; + + for (const diagnostic of diagnostics) { + const codeAction = codeActionsMap[diagnostic.range.start.line + '|' + diagnostic.range.start.character]; + if (codeAction) { + + if (fileType === 'pom.xml') { + // add RedHat repository recommendation command to action + codeAction.command = { + title: 'RedHat repository recommendation', + command: globalConfig.triggerRHRepositoryRecommendationNotification, + }; + } + + codeActions.push(codeAction); + + } + if (!hasRhdaDiagonostic) { + hasRhdaDiagonostic = diagnostic.source === RHDA_DIAGNOSTIC_SOURCE; + } + } + if (globalConfig.triggerFullStackAnalysis && hasRhdaDiagonostic) { + codeActions.push(generateFullStackAnalysisAction()); + } + return codeActions; +} + +function generateFullStackAnalysisAction(): CodeAction { + return { + title: 'Detailed Vulnerability Report', + kind: CodeActionKind.QuickFix, + command: { + title: 'Analytics Report', + command: globalConfig.triggerFullStackAnalysis, + } + }; +} + +export { getDiagnosticsCodeActions }; \ No newline at end of file diff --git a/src/collector.ts b/src/collector.ts index 3b8479c7..58c2d0f5 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -59,7 +59,10 @@ export class KeyValueEntry implements IKeyValueEntry { } export class Variant implements IVariant { - constructor(public type: ValueType, public object: any) { } + constructor( + public type: ValueType, + public object: any + ) { } } /* String value with position */ diff --git a/src/componentAnalysis.ts b/src/componentAnalysis.ts new file mode 100644 index 00000000..2fd5d15d --- /dev/null +++ b/src/componentAnalysis.ts @@ -0,0 +1,112 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { connection } from './server'; +import { globalConfig } from './config'; +import { isDefined } from './utils'; + +import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; + +/* Source specification */ +interface ISource { + id: string; + dependencies: any[]; +} + +class Source implements ISource { + constructor( + public id: string, + public dependencies: any[] + ) {} +} + +/* Dependency Data specification */ +interface IDependencyData { + sourceId: string; + issuesCount: number; + highestVulnerabilitySeverity: string; +} + +class DependencyData implements IDependencyData { + constructor( + public sourceId: string, + public issuesCount: number, + public highestVulnerabilitySeverity: string + ) {} +} + +/* Dependency Analysis Response specification */ +interface IAnalysisResponse { + dependencies: Map; +} + +class AnalysisResponse implements IAnalysisResponse { + dependencies: Map = new Map(); + + constructor(resData: exhort.AnalysisReport) { + + const failedProviders: string[] = []; + const sources: Source[] = []; + + if (isDefined(resData, 'providers')) { + Object.entries(resData.providers).map(([providerName, providerData]) => { + if (isDefined(providerData, 'status', 'ok') && isDefined(providerData, 'sources') && providerData.status.ok) { + Object.entries(providerData.sources).map(([sourceName, sourceData]) => { + sources.push(new Source(`${providerName}-${sourceName}`, isDefined(sourceData, 'dependencies') ? sourceData.dependencies : [])); + }); + } else { + failedProviders.push(providerName); + } + }); + + if (failedProviders.length !== 0) { + const errMsg = `The component analysis couldn't fetch data from the following providers: [${failedProviders.join(', ')}]`; + connection.console.warn(errMsg); + connection.sendNotification('caSimpleWarning', errMsg); + } + + sources.forEach(source => { + source.dependencies.forEach(d => { + if (isDefined(d, 'ref') && isDefined(d, 'issues')) { + const dd = new DependencyData(source.id, d.issues.length, isDefined(d, 'highestVulnerability', 'severity') ? d.highestVulnerability.severity : 'UNKNOWN'); + if (this.dependencies[d.ref] === undefined) { + this.dependencies[d.ref] = []; + } + this.dependencies[d.ref].push(dd); + } + }); + }); + } + } +} + +async function componentAnalysisService (fileType: string, contents: string): Promise { + + // set up configuration options for the component analysis request + const options = { + 'RHDA_TOKEN': globalConfig.telemetryId, + 'RHDA_SOURCE': globalConfig.utmSource, + 'EXHORT_DEV_MODE': globalConfig.exhortDevMode, + 'MATCH_MANIFEST_VERSIONS': globalConfig.matchManifestVersions, + 'EXHORT_MVN_PATH': globalConfig.exhortMvnPath, + 'EXHORT_NPM_PATH': globalConfig.exhortNpmPath, + 'EXHORT_GO_PATH': globalConfig.exhortGoPath, + 'EXHORT_PYTHON3_PATH': globalConfig.exhortPython3Path, + 'EXHORT_PIP3_PATH': globalConfig.exhortPip3Path, + 'EXHORT_PYTHON_PATH': globalConfig.exhortPythonPath, + 'EXHORT_PIP_PATH': globalConfig.exhortPipPath + }; + if (globalConfig.exhortSnykToken !== '') { + options['EXHORT_SNYK_TOKEN'] = globalConfig.exhortSnykToken; + } + + // get component analysis as JSON object + const componentAnalysisJson = await exhort.componentAnalysis(fileType, contents, options); + + return new AnalysisResponse(componentAnalysisJson); +} + +export { componentAnalysisService, DependencyData }; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 8c18ae4d..2e0c2bb6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,9 +5,10 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -class Config +export class Config { - provideFullstackAction: boolean; + triggerFullStackAnalysis: string; + triggerRHRepositoryRecommendationNotification: string; telemetryId: string; utmSource: string; exhortDevMode: string; @@ -23,7 +24,8 @@ class Config constructor() { // init child process configuration with parent process environment data - this.provideFullstackAction = (process.env.VSCEXT_PROVIDE_FULLSTACK_ACTION || '') === 'true'; + this.triggerFullStackAnalysis = process.env.VSCEXT_TRIGGER_FULL_STACK_ANALYSIS || ''; + this.triggerRHRepositoryRecommendationNotification = process.env.VSCEXT_TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION || ''; this.telemetryId = process.env.VSCEXT_TELEMETRY_ID || ''; this.utmSource = process.env.VSCEXT_UTM_SOURCE || ''; this.exhortDevMode = process.env.VSCEXT_EXHORT_DEV_MODE || 'false'; @@ -37,8 +39,20 @@ class Config this.exhortPythonPath = process.env.VSCEXT_EXHORT_PYTHON_PATH || 'python'; this.exhortPipPath = process.env.VSCEXT_EXHORT_PIP_PATH || 'pip'; } + + updateConfig( data: any ) { + this.exhortSnykToken = data.redHatDependencyAnalytics.exhortSnykToken; + this.matchManifestVersions = data.redHatDependencyAnalytics.matchManifestVersions ? 'true' : 'false'; + this.exhortMvnPath = data.mvn.executable.path || 'mvn'; + this.exhortNpmPath = data.npm.executable.path || 'npm'; + this.exhortGoPath = data.go.executable.path || 'go'; + this.exhortPython3Path = data.python3.executable.path || 'python3'; + this.exhortPip3Path = data.pip3.executable.path || 'pip3'; + this.exhortPythonPath = data.python.executable.path || 'python'; + this.exhortPipPath = data.pip.executable.path || 'pip'; + } } -const config = new Config(); +const globalConfig = new Config(); -export { config }; +export { globalConfig }; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..977ae3c7 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,14 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +/** + * Commonly used constants + */ + +// RHDA source +export const RHDA_DIAGNOSTIC_SOURCE = '\nRed Hat Dependency Analytics Plugin'; +// version placeholder for dependency template +export const VERSION_PLACEHOLDER: string = '__VERSION__'; diff --git a/src/consumers.ts b/src/consumers.ts deleted file mode 100644 index dbf48c82..00000000 --- a/src/consumers.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat - * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; -import { IDependency } from './collector'; -import { getRange } from './utils'; -import { Vulnerability } from './vulnerability'; -import { VulnerabilityAggregator } from './aggregators'; -import { Diagnostic, CodeAction } from 'vscode-languageserver'; - -/* Descriptor describing what key-path to extract from the document */ -interface IBindingDescriptor { - path: Array; -} - -/* Bind & return the part of `obj` as described by `desc` */ -const bindObject = (obj: any, desc: IBindingDescriptor) => { - let bind = obj; - for (const elem of desc.path) { - if (elem in bind) { - bind = bind[elem]; - } else { - return null; - } - } - return bind; -}; - -/* Arbitrary metadata consumer interface */ -interface IConsumer { - refbinding: IBindingDescriptor; - ref: string; - consume(data: any): boolean; -} - -/* Generic `T` producer */ -interface IProducer { - produce(): T; -} - -/* Each pipeline item is defined as a single consumer and producer pair */ -interface IPipelineItem extends IConsumer, IProducer { } - -/* House bunches of `IPipelineItem`'s */ -interface IPipeline { - item: IPipelineItem; - run(data: any): T; -} - -/* Diagnostics producer type */ -type DiagnosticProducer = IProducer; - -/* Diagnostics pipeline implementation */ -class DiagnosticsPipeline implements IPipeline -{ - item: IPipelineItem; - dependency: IDependency; - config: any; - diagnostics: Array; - uri: string; - vulnerabilityAggregator: VulnerabilityAggregator; - constructor(engine: any, dependency: IDependency, config: any, diags: Array, - vulnerabilityAggregator: VulnerabilityAggregator, uri: string) { - this.item = new engine(dependency, config); - this.dependency = dependency; - this.config = config; - this.diagnostics = diags; - this.uri = uri; - this.vulnerabilityAggregator = vulnerabilityAggregator; - } - - run(data: any): Vulnerability { - if (this.item.consume(data)) { - const vulnerability = this.item.produce(); - const aggVulnerability = this.vulnerabilityAggregator.aggregate(vulnerability); - if (this.vulnerabilityAggregator.isNewVulnerability) { - const aggDiagnostic = aggVulnerability.getDiagnostic(); - - // if (aggVulnerability.recommendation !== null && aggVulnerability.issuesCount === 0) { - // let codeAction: CodeAction = { - // title: `Switch to version ${aggVulnerability.recommendationVersion}`, - // diagnostics: [aggDiagnostic], - // kind: CodeActionKind.QuickFix, - // edit: { - // changes: { - // } - // } - // }; - // codeAction.edit.changes[this.uri] = [{ - // range: aggDiagnostic.range, - // newText: vulnerability.replacement.replace(VERSION_TEMPLATE, aggVulnerability.recommendationVersion) - // }]; - // codeActionsMap[aggDiagnostic.range.start.line + '|' + aggDiagnostic.range.start.character] = codeAction; - // } - // if (aggVulnerability.remediations && Object.keys(aggVulnerability.remediations).length > 0 && aggVulnerability.issuesCount > 0) { - // for (const cve of Object.keys(aggVulnerability.remediations)) { - - // let version = aggVulnerability.remediations[cve][`${aggVulnerability.ecosystem}Package`].split('@')[1]; - // let codeAction: CodeAction = { - // title: `Switch to version ${version} for ${cve}`, - // diagnostics: [aggDiagnostic], - // kind: CodeActionKind.QuickFix, - // edit: { - // changes: { - // } - // } - // }; - // codeAction.edit.changes[this.uri] = [{ - // range: aggDiagnostic.range, - // newText: vulnerability.replacement.replace(VERSION_TEMPLATE, version) - // }]; - // codeActionsMap[aggDiagnostic.range.start.line + '|' + aggDiagnostic.range.start.character] = codeAction; - // } - // } - - if (aggDiagnostic) { - this.diagnostics.push(aggDiagnostic); - } - } - } - return; - } -} - -/* A consumer that uses the binding interface to consume a metadata object */ -class AnalysisConsumer implements IConsumer { - refbinding: IBindingDescriptor; - issuesBinding: IBindingDescriptor; - // recommendationBinding: IBindingDescriptor; - // recommendationNameBinding: IBindingDescriptor; - // recommendationVersionBinding: IBindingDescriptor; - // remediationsBinding: IBindingDescriptor; - highestVulnerabilityBinding: IBindingDescriptor; - highestVulnerabilitySeverityBinding: IBindingDescriptor; - ref: string = null; - issues: any = null; - issuesCount: number = 0; - // recommendation: any = null; - // recommendationName: string = null; - // recommendationVersion: string = null; - // remediations: any = null; - highestVulnerability: any = null; - highestVulnerabilitySeverity: string = null; - constructor(public config: any) { } - consume(data: any): boolean { - if (this.refbinding !== null) { - this.ref = bindObject(data, this.refbinding); - } - if (this.issuesBinding !== null) { - this.issues = bindObject(data, this.issuesBinding); - this.issuesCount = this.issues !== null ? this.issues.length : 0; - } - // if (this.recommendationBinding !== null) { - // this.recommendation = bindObject(data, this.recommendationBinding); - // } - // if (this.recommendation !== null && this.recommendationNameBinding !== null) { - // this.recommendationName = bindObject(data, this.recommendationNameBinding); - // } - // if (this.recommendation !== null && this.recommendationVersionBinding !== null) { - // this.recommendationVersion = bindObject(data, this.recommendationVersionBinding); - // } - // if (this.remediationsBinding !== null) { - // this.remediations = bindObject(data, this.remediationsBinding); - // } - if (this.highestVulnerabilityBinding !== null) { - this.highestVulnerability = bindObject(data, this.highestVulnerabilityBinding); - } - if (this.highestVulnerability !== null && this.highestVulnerabilitySeverityBinding !== null) { - this.highestVulnerabilitySeverity = bindObject(data, this.highestVulnerabilitySeverityBinding); - } - return this.ref !== null; - } -} - -/* Report CVEs in found dependencies */ -class SecurityEngine extends AnalysisConsumer implements DiagnosticProducer { - constructor(public context: IDependency, config: any) { - super(config); - this.refbinding = { path: ['ref'] }; - this.issuesBinding = { path: ['issues'] }; - // this.recommendationBinding = { path: ['recommendation'] }; - // this.recommendationNameBinding = { path: ['recommendation', 'name'] }; - // this.recommendationVersionBinding = { path: ['recommendation', 'version'] }; - // this.remediationsBinding = { path: ['remediations'] }; - this.highestVulnerabilityBinding = { path: ['highestVulnerability'] }; - this.highestVulnerabilitySeverityBinding = { path: ['highestVulnerability', 'severity'] }; - } - - produce(): Vulnerability { - return new Vulnerability( - getRange(this.context), - this.ref, - this.issuesCount, - // this.recommendation, - // this.recommendationName, - // this.recommendationVersion, - // this.remediations, - this.highestVulnerabilitySeverity, - this.context.context ? this.context.context.value : null - ); - } -} - -const codeActionsMap = new Map(); - -export { DiagnosticsPipeline, SecurityEngine, codeActionsMap }; \ No newline at end of file diff --git a/src/diagnosticsHandler.ts b/src/diagnosticsHandler.ts new file mode 100644 index 00000000..40656ee5 --- /dev/null +++ b/src/diagnosticsHandler.ts @@ -0,0 +1,119 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { Diagnostic, CodeAction } from 'vscode-languageserver'; +import { DependencyMap, IDependencyProvider } from './collector'; +import { componentAnalysisService, DependencyData } from './componentAnalysis'; +import { Vulnerability } from './vulnerability'; +import { getRange } from './utils'; +import { connection } from './server'; +import * as path from 'path'; + +/* Diagnostics Pipeline specification */ +interface IDiagnosticsPipeline { + clearDiagnostics(): void; + reportDiagnostics(): void; + runDiagnostics(dependencies: Map): void; +} + +class DiagnosticsPipeline implements IDiagnosticsPipeline { + private diagnostics: Diagnostic[] = []; + private vulnCount: number = 0; + + constructor( + private provider: IDependencyProvider, + private dependencyMap: DependencyMap, + private diagnosticFilePath: string, + ) {} + + clearDiagnostics() { + connection.sendDiagnostics({ uri: this.diagnosticFilePath, diagnostics: [] }); + connection.sendNotification('caNotification', { + done: false, + uri: this.diagnosticFilePath, + }); + } + + reportDiagnostics() { + connection.sendNotification('caNotification', { + done: true, + uri: this.diagnosticFilePath, + diagCount: this.diagnostics.length, + vulnCount: this.vulnCount, + }); + } + + runDiagnostics(dependencies: Map) { + Object.entries(dependencies).map(([ref, dependencyData]) => { + const dependency = this.dependencyMap.get(ref.split('@')[0].replace(`pkg:${this.provider.ecosystem}/`, '')); + if (dependency !== undefined) { + const vulnerability = new Vulnerability( + this.provider, + getRange(dependency), + ref, + dependencyData, + ); + + const vulnerabilityDiagnostic = vulnerability.getDiagnostic(); + if (vulnerabilityDiagnostic) { + this.diagnostics.push(vulnerabilityDiagnostic); + } + + const totalIssuesCount: number = dependencyData.reduce( + (sum, currentItem) => sum + currentItem.issuesCount, + 0 // Initial value for the sum + ); + this.vulnCount += totalIssuesCount; + } + connection.sendDiagnostics({ uri: this.diagnosticFilePath, diagnostics: this.diagnostics }); + }); + } +} + +async function performDiagnostics(diagnosticFilePath: string, contents: string, provider: IDependencyProvider) { + + // collect dependencies from manifest + let dependencies = null; + dependencies = await provider.collect(contents) + .catch(error => { + connection.console.warn(`Error: ${error}`); + connection.sendNotification('caError', { + data: error, + uri: diagnosticFilePath, + }); + return; + }); + + // map dependencies + const dependencyMap = new DependencyMap(dependencies); + + // init Diagnostics Pipeline + const diagnosticsPipeline = new DiagnosticsPipeline(provider, dependencyMap, diagnosticFilePath); + + // clear Diagnostics + diagnosticsPipeline.clearDiagnostics(); + + // execute Component Analysis + const analysis = componentAnalysisService(path.basename(diagnosticFilePath), contents) + .then(response => { + diagnosticsPipeline.runDiagnostics(response.dependencies); + }) + .catch(error => { + const errMsg = `Component Analysis error. ${error}`; + connection.console.warn(errMsg); + connection.sendNotification('caSimpleWarning', errMsg); + return; + }); + + await analysis; + + // report Diagnostics results + diagnosticsPipeline.reportDiagnostics(); +} + +export const codeActionsMap = new Map(); + +export { performDiagnostics }; \ No newline at end of file diff --git a/src/fileHandler.ts b/src/fileHandler.ts new file mode 100644 index 00000000..2d9fb3ac --- /dev/null +++ b/src/fileHandler.ts @@ -0,0 +1,106 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as path from 'path'; + +import { Connection } from 'vscode-languageserver'; +import { performDiagnostics } from './diagnosticsHandler'; +import { DependencyProvider as PackageJson } from './providers/package.json'; +import { DependencyProvider as PomXml } from './providers/pom.xml'; +import { DependencyProvider as GoMod } from './providers/go.mod'; +import { DependencyProvider as RequirementsTxt } from './providers/requirements.txt'; + +enum EventStream { + Invalid, + Diagnostics +} + +interface IAnalysisFileHandlerCallback { + (uri: string, contents: string): void; +} + +interface IAnalysisFileHandler { + stream: EventStream; + matcher: RegExp; + callback: IAnalysisFileHandlerCallback; +} + +class AnalysisFileHandler implements IAnalysisFileHandler { + matcher: RegExp; + constructor( + public stream: EventStream, + matcher: string, + public callback: IAnalysisFileHandlerCallback + ) { + this.matcher = new RegExp(matcher); + } +} + +interface IAnalysisFiles { + handlers: Array; + fileData: Map; + on(stream: EventStream, matcher: string, cb: IAnalysisFileHandlerCallback): IAnalysisFiles; + run(stream: EventStream, uri: string, file: string, contents: string): any; +} + +class AnalysisFiles implements IAnalysisFiles { + constructor( + public handlers: Array = [], + public fileData: Map = new Map() + ) {} + on(stream: EventStream, matcher: string, cb: IAnalysisFileHandlerCallback): IAnalysisFiles { + this.handlers.push(new AnalysisFileHandler(stream, matcher, cb)); + return this; + } + run(stream: EventStream, uri: string, fileName: string, contents: string): any { + for (const handler of this.handlers) { + if (handler.stream === stream && handler.matcher.test(fileName)) { + return handler.callback(uri, contents); + } + } + } +} + +const files = new AnalysisFiles(); + +files.on(EventStream.Diagnostics, '^package\\.json$', (uri, contents) => { + performDiagnostics(uri, contents, new PackageJson()); +}); + +files.on(EventStream.Diagnostics, '^pom\\.xml$', (uri, contents) => { + performDiagnostics(uri, contents, new PomXml()); +}); + +files.on(EventStream.Diagnostics, '^go\\.mod$', (uri, contents) => { + performDiagnostics(uri, contents, new GoMod()); +}); + +files.on(EventStream.Diagnostics, '^requirements\\.txt$', (uri, contents) => { + performDiagnostics(uri, contents, new RequirementsTxt()); +}); + +interface IAnalysisLSPServer { + conn: Connection; + files: IAnalysisFiles; + + handleFileEvent(uri: string, contents: string): void; +} + +class AnalysisLSPServer implements IAnalysisLSPServer { + files: IAnalysisFiles = files; + + constructor( + public conn: Connection, + ) {} + + handleFileEvent(uri: string, contents: string): void { + const fileName = path.basename(uri); + this.files.fileData[uri] = contents; + this.files.run(EventStream.Diagnostics, uri, fileName, contents); + } +} + +export { IAnalysisLSPServer, AnalysisLSPServer }; \ No newline at end of file diff --git a/src/providers/pom.xml.ts b/src/providers/pom.xml.ts index 15c58832..5cfaf7a2 100644 --- a/src/providers/pom.xml.ts +++ b/src/providers/pom.xml.ts @@ -2,7 +2,7 @@ import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector'; import { parse, DocumentCstNode } from '@xml-tools/parser'; import { buildAst, accept, XMLElement, XMLDocument } from '@xml-tools/ast'; -import { VERSION_TEMPLATE } from '../utils'; +import { VERSION_PLACEHOLDER } from '../constants'; export class DependencyProvider implements IDependencyProvider { private xmlDocAst: XMLDocument; @@ -82,7 +82,7 @@ export class DependencyProvider implements IDependencyProvider { template += `${dep.textContents[idx++].text}<${e.name}>${e.textContents[0].text}`; } }); - template += `${margin}${VERSION_TEMPLATE}`; + template += `${margin}${VERSION_PLACEHOLDER}`; template += `${dep.textContents[idx].text}`; return template; }; diff --git a/src/server.ts b/src/server.ts index ad40ffc4..06d9b4ad 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,47 +5,26 @@ 'use strict'; import * as path from 'path'; - -import { - createConnection, - TextDocuments, InitializeResult, CodeAction, CodeActionKind, - ProposedFeatures -} from 'vscode-languageserver/node'; - -import { DependencyProvider as PackageJson } from './providers/package.json'; -import { DependencyProvider as PomXml } from './providers/pom.xml'; -import { DependencyProvider as GoMod } from './providers/go.mod'; -import { DependencyProvider as RequirementsTxt } from './providers/requirements.txt'; -import { DependencyMap, IDependencyProvider } from './collector'; -import { SecurityEngine, DiagnosticsPipeline, codeActionsMap } from './consumers'; -import { NoopVulnerabilityAggregator, MavenVulnerabilityAggregator } from './aggregators'; -import { ANALYTICS_SOURCE } from './vulnerability'; -import { config } from './config'; import { TextDocumentSyncKind, Connection, DidChangeConfigurationNotification } from 'vscode-languageserver'; +import { createConnection, TextDocuments, InitializeResult, CodeAction, ProposedFeatures } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; - +import { globalConfig } from './config'; +import { AnalysisLSPServer } from './fileHandler'; +import { getDiagnosticsCodeActions } from './codeActionHandler'; -enum EventStream { - Invalid, - Diagnostics -} +// declare timeout identifier to track delays for server.handleFileEvent execution +let checkDelay: NodeJS.Timeout; // Create a connection for the server, using Node's IPC as a transport. -// Include all preview / proposed LSP features. const connection: Connection = createConnection(ProposedFeatures.all); const documents: TextDocuments = new TextDocuments(TextDocument); documents.listen(connection); -// Set up the connection's initialization event handler. -let triggerFullStackAnalysis: string; -let triggerRHRepositoryRecommendationNotification: string; +// Sets up the connection's initialization event handler. let hasConfigurationCapability: boolean = false; connection.onInitialize((params): InitializeResult => { const capabilities = params.capabilities; - triggerFullStackAnalysis = params.initializationOptions.triggerFullStackAnalysis; - triggerRHRepositoryRecommendationNotification = params.initializationOptions.triggerRHRepositoryRecommendationNotification; hasConfigurationCapability = !!( capabilities.workspace && !!capabilities.workspace.configuration ); @@ -57,266 +36,15 @@ connection.onInitialize((params): InitializeResult => { }; }); -// Defining settings for Red Hat Dependency Analytics -interface RedhatDependencyAnalyticsSettings { - exhortSnykToken: string; - matchManifestVersions: string; - exhortMvnPath: string; - exhortNpmPath: string; - exhortGoPath: string; - exhortPython3Path: string; - exhortPip3Path: string; - exhortPythonPath: string; - exhortPipPath: string; -} - -// Initializing default settings for Red Hat Dependency Analytics -const defaultSettings: RedhatDependencyAnalyticsSettings = { - exhortSnykToken: config.exhortSnykToken, - matchManifestVersions: config.matchManifestVersions, - exhortMvnPath: config.exhortMvnPath, - exhortNpmPath: config.exhortNpmPath, - exhortGoPath: config.exhortGoPath, - exhortPython3Path: config.exhortPython3Path, - exhortPip3Path: config.exhortPip3Path, - exhortPythonPath: config.exhortPythonPath, - exhortPipPath: config.exhortPipPath -}; - -// Creating a mutable variable to hold the global settings for Red Hat Dependency Analytics. -let globalSettings: RedhatDependencyAnalyticsSettings = defaultSettings; - -interface IFileHandlerCallback { - (uri: string, name: string, contents: string): void; -} - -interface IAnalysisFileHandler { - matcher: RegExp; - stream: EventStream; - callback: IFileHandlerCallback; -} - -interface IAnalysisFiles { - handlers: Array; - fileData: Map; - on(stream: EventStream, matcher: string, cb: IFileHandlerCallback): IAnalysisFiles; - run(stream: EventStream, uri: string, file: string, contents: string): any; -} - -class AnalysisFileHandler implements IAnalysisFileHandler { - matcher: RegExp; - constructor(matcher: string, public stream: EventStream, public callback: IFileHandlerCallback) { - this.matcher = new RegExp(matcher); - } -} - -class AnalysisFiles implements IAnalysisFiles { - handlers: Array; - fileData: Map; - constructor() { - this.handlers = []; - this.fileData = new Map(); - } - on(stream: EventStream, matcher: string, cb: IFileHandlerCallback): IAnalysisFiles { - this.handlers.push(new AnalysisFileHandler(matcher, stream, cb)); - return this; - } - run(stream: EventStream, uri: string, file: string, contents: string): any { - for (const handler of this.handlers) { - if (handler.stream === stream && handler.matcher.test(file)) { - return handler.callback(uri, file, contents); - } - } - } -} - -interface IAnalysisLSPServer { - conn: Connection; - files: IAnalysisFiles; - - handleFileEvent(uri: string, contents: string): void; -} - -class AnalysisLSPServer implements IAnalysisLSPServer { - constructor(public conn: Connection, public files: IAnalysisFiles) { } - - handleFileEvent(uri: string, contents: string): void { - const pathName = new URL(uri).pathname; - const fileName = path.basename(pathName); - - this.files.fileData[uri] = contents; - - this.files.run(EventStream.Diagnostics, uri, fileName, contents); - } -} - -const files: IAnalysisFiles = new AnalysisFiles(); -const server: IAnalysisLSPServer = new AnalysisLSPServer(connection, files); - -// total counts of known security vulnerabilities -class VulnCount { - issuesCount: number = 0; -} -// Generate summary notification message for vulnerability analysis -const getCAmsg = (deps, diagnostics, vulnCount): string => { - let msg = `Scanned ${deps.length} ${deps.length === 1 ? 'dependency' : 'dependencies'}, `; - - if (diagnostics.length > 0) { - const c = vulnCount.issuesCount; - const vulStr = (count: number) => count === 1 ? 'Vulnerability' : 'Vulnerabilities'; - msg = c > 0 ? `flagged ${c} Known Security ${vulStr(c)} along with quick fixes` : 'No potential security vulnerabilities found'; - } else { - msg += 'No potential security vulnerabilities found'; - } - - return msg; -}; - -/* Runs DiagnosticPileline to consume dependencies and generate Diagnostic[] */ -function runPipeline(dependencies, diagnostics, packageAggregator, diagnosticFilePath, pkgMap: DependencyMap, vulnCount, provider: IDependencyProvider) { - dependencies.forEach(d => { - // match dependency with dependency from package map - const pkg = pkgMap.get(d.ref.split('@')[0].replace(`pkg:${provider.ecosystem}/`, '')); - // if dependency mached, run diagnostic - if (pkg !== undefined) { - const pipeline = new DiagnosticsPipeline(SecurityEngine, pkg, config, diagnostics, packageAggregator, diagnosticFilePath); - pipeline.run(d); - const secEng = pipeline.item as SecurityEngine; - vulnCount.issuesCount += secEng.issuesCount; - } - }); - connection.sendDiagnostics({ uri: diagnosticFilePath, diagnostics: diagnostics }); - connection.console.log(`sendDiagnostics: ${diagnostics?.length}`); -} - -// Fetch Vulnerabilities by component analysis API call -const fetchVulnerabilities = async (fileType: string, reqData: string) => { - - // set up configuration options for the component analysis request - const options = { - 'RHDA_TOKEN': config.telemetryId, - 'RHDA_SOURCE': config.utmSource, - 'EXHORT_DEV_MODE': config.exhortDevMode, - 'MATCH_MANIFEST_VERSIONS': globalSettings.matchManifestVersions, - 'EXHORT_MVN_PATH': globalSettings.exhortMvnPath, - 'EXHORT_NPM_PATH': globalSettings.exhortNpmPath, - 'EXHORT_GO_PATH': globalSettings.exhortGoPath, - 'EXHORT_PYTHON3_PATH': globalSettings.exhortPython3Path, - 'EXHORT_PIP3_PATH': globalSettings.exhortPip3Path, - 'EXHORT_PYTHON_PATH': globalSettings.exhortPythonPath, - 'EXHORT_PIP_PATH': globalSettings.exhortPipPath - }; - if (globalSettings.exhortSnykToken !== '') { - options['EXHORT_SNYK_TOKEN'] = globalSettings.exhortSnykToken; - } - - // get component analysis in JSON format - const componentAnalysisJson = await exhort.componentAnalysis(fileType, reqData, options); - - // check vulnerability provider statuses - const ko = []; - componentAnalysisJson.summary.providerStatuses.forEach(ps => { - if (!ps.ok) { - ko.push(ps.provider); - } - }); - // issue warning if failed to fetch data from providers - if (ko.length !== 0) { - const errMsg = `The Component Analysis couldn't fetch data from the following providers: [${ko}]`; - connection.console.warn(errMsg); - connection.sendNotification('caSimpleWarning', errMsg); - } - - return componentAnalysisJson; -}; - -const sendDiagnostics = async (diagnosticFilePath: string, contents: string, provider: IDependencyProvider) => { - - // get dependencies from response before firing diagnostics. - const getDepsAndRunPipeline = response => { - let deps = []; - if (response.dependencies && response.dependencies.length > 0) { - deps = response.dependencies; - } - runPipeline(deps, diagnostics, packageAggregator, diagnosticFilePath, pkgMap, vulnCount, provider); - }; - - // clear all diagnostics - connection.sendDiagnostics({ uri: diagnosticFilePath, diagnostics: [] }); - connection.sendNotification('caNotification', { - data: 'Checking for security vulnerabilities ...', - done: false, - uri: diagnosticFilePath, - }); - - // collect dependencies from manifest - let deps = null; - try { - const start = new Date().getTime(); - deps = await provider.collect(contents); - const end = new Date().getTime(); - connection.console.log(`manifest parse took ${end - start} ms, found ${deps.length} deps`); - } catch (error) { - connection.console.warn(`Error: ${error}`); - connection.sendNotification('caError', { - data: error, - uri: diagnosticFilePath, - }); - return; +// Registers a callback when the connection is fully initialized. +connection.onInitialized(() => { + if (hasConfigurationCapability) { + // Register for all configuration changes. + connection.client.register(DidChangeConfigurationNotification.type, undefined); } - - // map dependencies - const pkgMap = new DependencyMap(deps); - - // init aggregator - const packageAggregator = provider.ecosystem === 'maven' ? new MavenVulnerabilityAggregator(provider) : new NoopVulnerabilityAggregator(provider); - - // init tracking components - const diagnostics = []; - const vulnCount = new VulnCount(); - const start = new Date().getTime(); - - // fetch vulnerabilities - const request = fetchVulnerabilities(path.basename(diagnosticFilePath), contents) - .then(getDepsAndRunPipeline) - .catch(error => { - const errMsg = `Component Analysis error. ${error}`; - connection.console.warn(errMsg); - connection.sendNotification('caSimpleWarning', errMsg); - return; - }); - await request; - - // report results - const end = new Date().getTime(); - connection.console.log(`fetch vulns took ${end - start} ms`); - connection.sendNotification('caNotification', { - data: getCAmsg(deps, diagnostics, vulnCount), - done: true, - uri: diagnosticFilePath, - diagCount: diagnostics.length || 0, - vulnCount: vulnCount.issuesCount, - }); -}; - -files.on(EventStream.Diagnostics, '^package\\.json$', (uri, name, contents) => { - sendDiagnostics(uri, contents, new PackageJson()); -}); - -files.on(EventStream.Diagnostics, '^pom\\.xml$', (uri, name, contents) => { - sendDiagnostics(uri, contents, new PomXml()); -}); - -files.on(EventStream.Diagnostics, '^go\\.mod$', (uri, name, contents) => { - sendDiagnostics(uri, contents, new GoMod()); -}); - -files.on(EventStream.Diagnostics, '^requirements\\.txt$', (uri, name, contents) => { - sendDiagnostics(uri, contents, new RequirementsTxt()); }); -// TRIGGERS -let checkDelay; +const server = new AnalysisLSPServer(connection); // triggered when document is opened connection.onDidOpenTextDocument((params) => { @@ -344,70 +72,22 @@ connection.onDidCloseTextDocument(() => { clearTimeout(checkDelay); }); -// Registering a callback when the connection is fully initialized. -connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register(DidChangeConfigurationNotification.type, undefined); - } -}); - // Registering a callback when the configuration changes. connection.onDidChangeConfiguration(() => { if (hasConfigurationCapability) { // Fetching the workspace configuration from the client. - server.conn.workspace.getConfiguration().then((data) => { + server.conn.workspace.getConfiguration() + .then((data) => { // Updating global settings based on the fetched configuration data. - globalSettings = ({ - exhortSnykToken: data.redHatDependencyAnalytics.exhortSnykToken, - matchManifestVersions: data.redHatDependencyAnalytics.matchManifestVersions ? 'true' : 'false', - exhortMvnPath: data.mvn.executable.path || 'mvn', - exhortNpmPath: data.npm.executable.path || 'npm', - exhortGoPath: data.go.executable.path || 'go', - exhortPython3Path: data.python3.executable.path || 'python3', - exhortPip3Path: data.pip3.executable.path || 'pip3', - exhortPythonPath: data.python.executable.path || 'python', - exhortPipPath: data.pip.executable.path || 'pip' - }); + globalConfig.updateConfig(data); }); } }); -const fullStackReportAction = (): CodeAction => ({ - title: 'Detailed Vulnerability Report', - kind: CodeActionKind.QuickFix, - command: { - command: triggerFullStackAnalysis, - title: 'Analytics Report', - } -}); - - connection.onCodeAction((params): CodeAction[] => { - const codeActions: CodeAction[] = []; - let hasAnalyticsDiagonostic: boolean = false; - for (const diagnostic of params.context.diagnostics) { - const codeAction = codeActionsMap[diagnostic.range.start.line + '|' + diagnostic.range.start.character]; - if (codeAction) { - - if (path.basename(params.textDocument.uri) === 'pom.xml') { - codeAction.command = { - title: 'RedHat repository recommendation', - command: triggerRHRepositoryRecommendationNotification, - }; - } - - codeActions.push(codeAction); - - } - if (!hasAnalyticsDiagonostic) { - hasAnalyticsDiagonostic = diagnostic.source === ANALYTICS_SOURCE; - } - } - if (config.provideFullstackAction && hasAnalyticsDiagonostic) { - codeActions.push(fullStackReportAction()); - } - return codeActions; + return getDiagnosticsCodeActions(params.context.diagnostics, path.basename(params.textDocument.uri)); }); -connection.listen(); \ No newline at end of file +connection.listen(); + +export { connection }; diff --git a/src/utils.ts b/src/utils.ts index 46bb25ba..07865131 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; + import { Position, Range } from 'vscode-languageserver'; import { IPositionedString, IPosition, IDependency } from './collector'; @@ -36,4 +37,12 @@ export const getRange = (dep: IDependency): Range => { }; -export const VERSION_TEMPLATE: string = '__VERSION__'; \ No newline at end of file +export function isDefined(obj: any, ...keys: string[]): boolean { + for (const key of keys) { + if (!obj || !obj[key]) { + return false; + } + obj = obj[key]; + } + return true; +} diff --git a/src/vulnerability.ts b/src/vulnerability.ts index e5e8c062..f16c135f 100644 --- a/src/vulnerability.ts +++ b/src/vulnerability.ts @@ -3,79 +3,43 @@ * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; + import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'; import { Range } from 'vscode-languageserver'; -import { VERSION_TEMPLATE } from './utils'; import { IDependencyProvider } from './collector'; - -const ANALYTICS_SOURCE = '\nRed Hat Dependency Analytics Plugin [Powered by Snyk]'; +import { DependencyData } from './componentAnalysis'; +import { RHDA_DIAGNOSTIC_SOURCE } from './constants'; /* Vulnerability data along with package name and version */ class Vulnerability { - provider: IDependencyProvider; - range: Range; - issuesCount: number; - ref: string; - // recommendation: any; - // recommendationName: string; - // recommendationVersion: string; - // remediations: any; - highestVulnerabilitySeverity: string; - replacement: string; - constructor( - range: Range, - ref: string, - issuesCount: number = 0, - // recommendation: any = null, - // recommendationName: string = null, - // recommendationVersion: string = null, - // remediations: any = null, - highestVulnerabilitySeverity: string = null, - replacement: string = null, - ) { - this.range = range; - this.ref = ref; - this.issuesCount = issuesCount || 0; - // this.recommendation = recommendation || null; - // this.recommendationName = recommendationName || ''; - // this.recommendationVersion = recommendationVersion || ''; - // this.remediations = remediations || null; - this.highestVulnerabilitySeverity = highestVulnerabilitySeverity || ''; - this.replacement = replacement || VERSION_TEMPLATE; - } + private provider: IDependencyProvider, + private range: Range, + private ref: string, + private dependencyData: DependencyData[], + ) {} getDiagnostic(): Diagnostic { - if (this.issuesCount === 0) { - return; -// diagSeverity = DiagnosticSeverity.Information; + const diagSeverity = DiagnosticSeverity.Error; -// if (this.recommendation !== null) { -// msg = `${this.ref} -// Recommendation: ${this.recommendationName}:${this.recommendationVersion}`; -// } else { -// msg = `${this.ref} -// Recommendation: No RedHat packages to recommend`; -// } - } + let msg: string = `${this.ref.replace(`pkg:${this.provider.ecosystem}/`, '')}`; - const diagSeverity = DiagnosticSeverity.Error; - const msg = `${this.ref.replace(`pkg:${this.provider.ecosystem}/`, '')} -Known security vulnerabilities: ${this.issuesCount} -Highest severity: ${this.highestVulnerabilitySeverity}`; -// msg = `${this.ref} -// Known security vulnerabilities: ${this.issuesCount} -// Highest severity: ${this.highestVulnerabilitySeverity} -// Has remediation: ${this.remediations && Object.keys(this.remediations).length > 0 ? 'Yes' : 'No'}`; + this.dependencyData.forEach(dm => { + msg += ` + +${dm.sourceId} vulnerability info: +Known security vulnerabilities: ${dm.issuesCount} +Highest severity: ${dm.highestVulnerabilitySeverity}`; + }); return { severity: diagSeverity, range: this.range, message: msg, - source: ANALYTICS_SOURCE, + source: RHDA_DIAGNOSTIC_SOURCE, }; } } -export { Vulnerability, ANALYTICS_SOURCE }; \ No newline at end of file +export { Vulnerability }; \ No newline at end of file From d3265e63c6eea0a17a543f8e1462e87d028628f3 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Sun, 12 Nov 2023 15:53:19 +0200 Subject: [PATCH 2/6] chore: simplify collector Signed-off-by: Ilona Shishov --- src/collector.ts | 118 +++++---------------- src/diagnosticsHandler.ts | 2 +- src/fileHandler.ts | 4 +- src/providers/go.mod.ts | 166 ++++++++++++++---------------- src/providers/package.json.ts | 57 +++++----- src/providers/pom.xml.ts | 123 +++++++++++----------- src/providers/requirements.txt.ts | 71 ++++++------- src/utils.ts | 50 +++++---- src/vulnerability.ts | 2 +- 9 files changed, 259 insertions(+), 334 deletions(-) diff --git a/src/collector.ts b/src/collector.ts index 58c2d0f5..d42a5099 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -6,65 +6,12 @@ import { Range } from 'vscode-languageserver'; -/* Determine what is the value */ -export enum ValueType { - Invalid, - String, - Integer, - Float, - Array, - Object, - Boolean, - Null -} - -/* Value variant */ -export interface IVariant { - type: ValueType; - object: any; -} - /* Line and column inside the JSON file */ export interface IPosition { line: number; column: number; } -/* Key/Value entry with positions */ -export interface IKeyValueEntry { - key: string; - value: IVariant; - keyPosition: IPosition; - valuePosition: IPosition; - context: string; - contextRange: Range; -} - -export class KeyValueEntry implements IKeyValueEntry { - key: string; - value: IVariant; - keyPosition: IPosition; - valuePosition: IPosition; - context: string; - contextRange: Range; - - constructor(k: string, pos: IPosition, v?: IVariant, vPos?: IPosition, c?: string, cRange?: Range) { - this.key = k; - this.keyPosition = pos; - this.value = v; - this.valuePosition = vPos; - this.context = c; - this.contextRange = cRange; - } -} - -export class Variant implements IVariant { - constructor( - public type: ValueType, - public object: any - ) { } -} - /* String value with position */ export interface IPositionedString { value: string; @@ -83,51 +30,40 @@ export interface IDependency { context: IPositionedContext; } -export interface IHashableDependency extends IDependency { - key(): string; -} - -/* Ecosystem provider interface */ -export interface IDependencyProvider { - ecosystem: string; - classes: Array; - collect(contents: string): Promise>; +/* Dependency class that can be created from `IKeyValueEntry` */ +export class Dependency implements IDependency { + constructor( + public name: IPositionedString, + public version: IPositionedString = {} as IPositionedString, + public context: IPositionedContext = {} as IPositionedContext, + ) {} } -/* Dependency class that can be created from `IKeyValueEntry` */ -export class Dependency implements IHashableDependency { - name: IPositionedString; - version: IPositionedString; - context: IPositionedContext; - constructor(dependency: IKeyValueEntry) { - this.name = { - value: dependency.key, - position: dependency.keyPosition - }; - this.version = { - value: dependency.value.object, - position: dependency.valuePosition - }; - if (dependency.context && dependency.contextRange) { - this.context = { - value: dependency.context, - range: dependency.contextRange - }; - } +export class DependencyMap { + mapper: Map; + constructor(deps: IDependency[]) { + this.mapper = new Map(deps.map(d => [d.name.value, d])); } - key(): string { - return `${this.name.value}`; + public get(key: string): IDependency { + return this.mapper.get(key); } } -export class DependencyMap { - mapper: Map; - constructor(deps: Array) { - this.mapper = new Map(deps.map(d => [d.key(), d])); +/* Ecosystem provider interface */ +export interface IDependencyProvider { + collect(contents: string): Promise; + resolveDependencyFromReference(ref: string): string; +} + +export class EcosystemDependencyResolver { + private ecosystem: string; + + constructor(ecosystem: string) { + this.ecosystem = ecosystem; } - public get(key: string): IHashableDependency { - return this.mapper.get(key); + resolveDependencyFromReference(ref: string): string { + return ref.replace(`pkg:${this.ecosystem}/`, ''); } -} +} \ No newline at end of file diff --git a/src/diagnosticsHandler.ts b/src/diagnosticsHandler.ts index 40656ee5..cbb59b14 100644 --- a/src/diagnosticsHandler.ts +++ b/src/diagnosticsHandler.ts @@ -48,7 +48,7 @@ class DiagnosticsPipeline implements IDiagnosticsPipeline { runDiagnostics(dependencies: Map) { Object.entries(dependencies).map(([ref, dependencyData]) => { - const dependency = this.dependencyMap.get(ref.split('@')[0].replace(`pkg:${this.provider.ecosystem}/`, '')); + const dependency = this.dependencyMap.get(this.provider.resolveDependencyFromReference(ref).split('@')[0]); if (dependency !== undefined) { const vulnerability = new Vulnerability( this.provider, diff --git a/src/fileHandler.ts b/src/fileHandler.ts index 2d9fb3ac..efc07174 100644 --- a/src/fileHandler.ts +++ b/src/fileHandler.ts @@ -40,7 +40,7 @@ class AnalysisFileHandler implements IAnalysisFileHandler { } interface IAnalysisFiles { - handlers: Array; + handlers: IAnalysisFileHandler[]; fileData: Map; on(stream: EventStream, matcher: string, cb: IAnalysisFileHandlerCallback): IAnalysisFiles; run(stream: EventStream, uri: string, file: string, contents: string): any; @@ -48,7 +48,7 @@ interface IAnalysisFiles { class AnalysisFiles implements IAnalysisFiles { constructor( - public handlers: Array = [], + public handlers: IAnalysisFileHandler[] = [], public fileData: Map = new Map() ) {} on(stream: EventStream, matcher: string, cb: IAnalysisFileHandlerCallback): IAnalysisFiles { diff --git a/src/providers/go.mod.ts b/src/providers/go.mod.ts index 973e740b..2fc0f99d 100644 --- a/src/providers/go.mod.ts +++ b/src/providers/go.mod.ts @@ -1,60 +1,83 @@ 'use strict'; -import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector'; - -/* Please note :: There was issue with semverRegex usage in the code. During run time, it extracts - * version with 'v' prefix, but this is not be behavior of semver in CLI and test environment. - * At the moment, using regex directly to extract version information without 'v' prefix. */ -//import semverRegex = require('semver-regex'); -function semVerRegExp(line: string): RegExpExecArray { - const regExp = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/ig; - return regExp.exec(line); -} +import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; +import { semVerRegExp } from '../utils' -class NaiveGomodParser { - constructor(contents: string) { - this.dependencies = NaiveGomodParser.parseDependencies(contents); +/* Process entries found in the go.mod file */ +export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { + replacementMap: Map = new Map(); + + constructor() { + super('golang'); // set ecosystem to 'golang' } - dependencies: Array; + static parseTxtDoc(contents: string): string[] { + return contents.split('\n'); + } - static getReplaceMap(line: string, index: number): any{ - // split the replace statements by '=>' - const parts: Array = line.replace('replace', '').replace('(', '').replace(')', '').trim().split('=>'); - const replaceWithVersion = semVerRegExp(parts[1]); - - // Skip lines without final version string - if (replaceWithVersion && replaceWithVersion.length > 0) { - const replaceTo: Array = (parts[0] || '').trim().split(' '); - const replaceToVersion = semVerRegExp(replaceTo[1]); - const replaceWith: Array = (parts[1] || '').trim().split(' '); - const replaceWithIndex = line.lastIndexOf(parts[1]); - const replaceEntry: IKeyValueEntry = new KeyValueEntry(replaceWith[0].trim(), { line: 0, column: 0 }); - replaceEntry.value = new Variant(ValueType.String, 'v' + replaceWithVersion[0]); - replaceEntry.valuePosition = { line: index + 1, column: (replaceWithIndex + replaceWithVersion.index) }; - const replaceDependency = new Dependency(replaceEntry); - const isReplaceToVersion: boolean = replaceToVersion && replaceToVersion.length > 0; - return {key: replaceTo[0].trim() + (isReplaceToVersion ? ('@v' + replaceToVersion[0]) : ''), value: replaceDependency}; + static clean(line: string): string { + return line.replace(/require|replace|\(|\)/g, '') + .replace(/\s+/g, ' ') + .trim(); + } + + static getDependencyData(line: string): { name: string, version: string, index: number } | null { + const versionMatches: RegExpExecArray = semVerRegExp(line); + if (versionMatches && versionMatches.length > 0) { + const depName = DependencyProvider.clean(line).split(' ')[0]; + return {name: depName, version: versionMatches[0], index: versionMatches.index} } return null; } - static applyReplaceMap(dep: IDependency, replaceMap: Map): IDependency { - let replaceDependency = replaceMap.get(dep.name.value + '@' + dep.version.value); - if (replaceDependency === undefined) { - replaceDependency = replaceMap.get(dep.name.value); - if(replaceDependency === undefined) { - return dep; - } + private registerReplacement(line: string, index: number) { + // split the replace statements by '=>' + const lineData: string[] = line.split('=>'); + if (lineData.length !== 2) return; + + let originalDepData = DependencyProvider.getDependencyData(lineData[0]); + const replacementDepData = DependencyProvider.getDependencyData(lineData[1]); + + if (!originalDepData) originalDepData = {name: DependencyProvider.clean(lineData[0]), version: null, index: null} + if (!replacementDepData) return; + + const replaceDependency = new Dependency( + { value: replacementDepData.name, position: { line: 0, column: 0 } }, + { value: 'v' + replacementDepData.version, position: { line: index + 1, column: (line.lastIndexOf(lineData[1]) + replacementDepData.index) } }, + ); + + this.replacementMap.set(originalDepData.name + (originalDepData.version ? ('@v' + originalDepData.version) : ''), replaceDependency); + } + + private parseLine(line: string, index: number): IDependency | null { + + line = line.split('//')[0]; // Remove comments + if (!DependencyProvider.clean(line)) return null; // Skip lines without dependencies + + if (line.includes('=>')) { + // stash replacement dependencies for replacement + this.registerReplacement(line, index); + return null; } - return replaceDependency; + + const depData = DependencyProvider.getDependencyData(line); + if (!depData) return null; + + return new Dependency( + { value: depData.name, position: { line: 0, column: 0 } }, + { value: 'v' + depData.version, position: { line: index + 1, column: depData.index } }, + ); + } + + private applyReplaceMap(dep: IDependency): IDependency { + return this.replacementMap.get(dep.name.value + '@' + dep.version.value) || this.replacementMap.get(dep.name.value) || dep; } - static parseDependencies(contents:string): Array { - const replaceMap = new Map(); - let isExcluded = false; - let goModDeps = contents.split('\n').reduce((dependencies, line, index) => { - // ignore excluded dependencies + private extractDependenciesFromLines(lines :string[]): IDependency[] { + let isExcluded: boolean = false; + const goModDeps: IDependency[] = lines.reduce((dependencies: IDependency[], line: string, index: number) => { + + // ignore excluded dependency lines and scopes if (line.includes('exclude')) { if (line.includes('(')) { isExcluded = true; @@ -68,57 +91,22 @@ class NaiveGomodParser { return dependencies; } - // skip any text after '//' - if (line.includes('//')) { - line = line.split('//')[0]; + // parse included lines for dependencies + const parsedDependency: IDependency = this.parseLine(line, index); + if (parsedDependency) { + dependencies.push(parsedDependency); } - // stash replacement dependencies for replacement - if (line.includes('=>')) { - const replaceEntry = NaiveGomodParser.getReplaceMap(line, index); - if (replaceEntry) { - replaceMap.set(replaceEntry.key, replaceEntry.value); - } - } else { - // Not using semver directly, look at comment on import statement. - const version = semVerRegExp(line); - // Skip lines without version string - if (version && version.length > 0) { - const parts: Array = line.replace('require', '').replace('(', '').replace(')', '').trim().split(' '); - const pkgName: string = (parts[0] || '').trim(); - // Ignore line starting with replace clause and empty package - if (pkgName.length > 0) { - const entry: IKeyValueEntry = new KeyValueEntry(pkgName, { line: 0, column: 0 }); - entry.value = new Variant(ValueType.String, 'v' + version[0]); - entry.valuePosition = { line: index + 1, column: version.index }; - // Push all direct and indirect modules present in go.mod (manifest) - dependencies.push(new Dependency(entry)); - } - } - } return dependencies; - }, []); - // apply replacement dependencies - goModDeps = goModDeps.map(goModDep => NaiveGomodParser.applyReplaceMap(goModDep, replaceMap)); - - // Return modules present in go.mod. - return [...goModDeps]; - } - parse(): Array { - return this.dependencies; - } -} + }, []); -/* Process entries found in the go.mod file */ -export class DependencyProvider implements IDependencyProvider { - ecosystem: string; - constructor(public classes: Array = ['dependencies']) { - this.ecosystem = 'golang'; + // apply replacement dependencies + return goModDeps.map(goModDep => this.applyReplaceMap(goModDep)); } - async collect(contents: string): Promise> { - const parser = new NaiveGomodParser(contents); - return parser.parse(); + async collect(contents: string): Promise { + const lines: string[] = DependencyProvider.parseTxtDoc(contents); + return this.extractDependenciesFromLines(lines); } } diff --git a/src/providers/package.json.ts b/src/providers/package.json.ts index 69c6ca8e..90872e0d 100644 --- a/src/providers/package.json.ts +++ b/src/providers/package.json.ts @@ -1,33 +1,40 @@ 'use strict'; import jsonAst from 'json-to-ast'; -import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector'; +import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; -export class DependencyProvider implements IDependencyProvider { - ecosystem: string; - constructor(public classes: Array = ['dependencies']) { - this.ecosystem = 'npm'; +export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { + private classes: string[] = ['dependencies']; + + constructor() { + super('npm'); // set ecosystem to 'npm' + } + + private parseJson(contents: string): jsonAst { + return jsonAst(contents || '{}'); + } + + private mapDependencies(jsonAst: jsonAst): IDependency[] { + return jsonAst.children + .filter(c => this.classes.includes(c.key.value)) + .flatMap(c => c.value.children) + .map(c => { + return new Dependency( + { value: c.key.value, position: {line: c.key.loc.start.line, column: c.key.loc.start.column + 1} }, + { value: c.value.value, position: {line: c.value.loc.start.line, column: c.value.loc.start.column + 1} }, + ); + }); } - async collect(contents: string): Promise> { - let ast: any; - try { - ast = jsonAst(contents || '{}'); - } catch (err) { - // doesn't make any sense to throw syntax errors. - if (err.name === 'SyntaxError') { - return []; - } - throw err; - } - return ast.children. - filter(c => this.classes.includes(c.key.value)). - flatMap(c => c.value.children). - map(c => { - const entry: IKeyValueEntry = new KeyValueEntry(c.key.value, {line: c.key.loc.start.line, column: c.key.loc.start.column + 1}); - entry.value = new Variant(ValueType.String, c.value.value); - entry.valuePosition = {line: c.value.loc.start.line, column: c.value.loc.start.column + 1}; - return new Dependency(entry); - }); + async collect(contents: string): Promise { + try { + const jsonAst: jsonAst = this.parseJson(contents); + return this.mapDependencies(jsonAst); + } catch (err) { + if (err instanceof SyntaxError) { + return []; + } + throw err; + } } } diff --git a/src/providers/pom.xml.ts b/src/providers/pom.xml.ts index 5cfaf7a2..0133cfa6 100644 --- a/src/providers/pom.xml.ts +++ b/src/providers/pom.xml.ts @@ -1,19 +1,22 @@ 'use strict'; -import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector'; +import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; import { parse, DocumentCstNode } from '@xml-tools/parser'; import { buildAst, accept, XMLElement, XMLDocument } from '@xml-tools/ast'; import { VERSION_PLACEHOLDER } from '../constants'; -export class DependencyProvider implements IDependencyProvider { - private xmlDocAst: XMLDocument; - ecosystem: string; +export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { - constructor(public classes: Array = ['dependencies']) { - this.ecosystem = 'maven'; + constructor() { + super('maven'); // set ecosystem to 'maven' } - private findRootNodes(document: XMLDocument, rootElementName: string): Array { - const properties: Array = []; + private parseXml(contents: string): XMLDocument { + const { cst, tokenVector } = parse(contents); + return buildAst(cst as DocumentCstNode, tokenVector); + } + + private findRootNodes(document: XMLDocument, rootElementName: string): XMLElement[] { + const properties: XMLElement[] = []; const propertiesElement = { // Will be invoked once for each Element node in the AST. visitXMLElement: (node: XMLElement) => { @@ -25,20 +28,30 @@ export class DependencyProvider implements IDependencyProvider { accept(document, propertiesElement); return properties; } + + private getXMLDependencies(xmlAst: XMLDocument): XMLElement[] { + const validElementNames = ['groupId', 'artifactId']; - private parseXml(contents: string): void { - const { cst, tokenVector } = parse(contents); - this.xmlDocAst = buildAst(cst as DocumentCstNode, tokenVector); + return this.findRootNodes(xmlAst, 'dependencies') + //must not be a dependency under dependencyManagement + .filter(e => { + const parentElement = e.parent as XMLElement | undefined; + return parentElement?.name !== 'dependencyManagement'; + }) + .map(node => node.subElements) + .flat(1) + .filter(e => e.name === 'dependency') + // must include all validElementNames + .filter(e => e.subElements.filter(elm => validElementNames.includes(elm.name)).length === validElementNames.length); } - private mapToDependency(deps: XMLElement[]): Array { + private mapDependencies(deps: XMLElement[]): IDependency[] { + class PomDependency { - public element: XMLElement; public groupId: XMLElement; public artifactId: XMLElement; public version: XMLElement; - constructor(element: XMLElement) { - this.element = element; + constructor(public element: XMLElement) { this.groupId = element.subElements.find(e => e.name === 'groupId'); this.artifactId = element.subElements.find(e => e.name === 'artifactId'); this.version = element.subElements.find(e => e.name === 'version'); @@ -52,74 +65,62 @@ export class DependencyProvider implements IDependencyProvider { } const toDependency = (d: PomDependency): Dependency => { - const dep: IKeyValueEntry = new KeyValueEntry( - `${d.groupId.textContents[0].text}/${d.artifactId.textContents[0].text}`, - { line: d.element.position.startLine, column: d.element.position.startColumn } - ); - dep.contextRange = { - start: { line: d.element.position.startLine - 1, character: d.element.position.startColumn - 1 }, - end: { line: d.element.position.endLine - 1, character: d.element.position.endColumn } - }; + const dep: Dependency = new Dependency( + { value: `${d.groupId.textContents[0].text}/${d.artifactId.textContents[0].text}`, + position: { line: d.element.position.startLine, column: d.element.position.startColumn } }, + ); + + dep.context = { value: '', range: { + start: { line: d.element.position.startLine - 1, character: d.element.position.startColumn - 1 }, + end: { line: d.element.position.endLine - 1, character: d.element.position.endColumn } + }, + } if (d.version && d.version.textContents.length > 0) { - dep.value = new Variant(ValueType.String, d.version.textContents[0].text); const versionVal = d.version.textContents[0]; - dep.valuePosition = { line: versionVal.position.startLine, column: versionVal.position.startColumn }; + dep.version = { + value: d.version.textContents[0].text, + position: { line: versionVal.position.startLine, column: versionVal.position.startColumn }, + } } else { - dep.value = new Variant(ValueType.String, ''); - dep.valuePosition = { line: 0, column: 0 }; - dep.context = dependencyTemplate(d.element); + dep.version = { + value: '', + position: { line: 0, column: 0 }, + } + dep.context.value = dependencyTemplate(d.element); } - return new Dependency(dep); + + return dep; }; const dependencyTemplate = (dep: XMLElement): string => { let template = ''; let idx = 0; const margin = dep.textContents[idx].text; + dep.subElements.forEach(e => { if (e.name !== 'version') { template += `${dep.textContents[idx++].text}<${e.name}>${e.textContents[0].text}`; } }); + template += `${margin}${VERSION_PLACEHOLDER}`; template += `${dep.textContents[idx].text}`; + return template; }; - - const purgeTestDeps = (nodes: XMLElement[]): Array => nodes - // no test dependencies - .filter(e => !e.subElements.find(elm => (elm.name === 'scope' && elm.textContents[0]?.text === 'test'))) - .map(e => new PomDependency(e)); - - const validDeps = purgeTestDeps(deps).filter(e => e.isValid()); - - const result = []; - validDeps.forEach((d) => { - result.push(toDependency(d)); - }); - return result; + + return deps + .filter(elm => !elm.subElements.find(subElm => (subElm.name === 'scope' && subElm.textContents[0]?.text === 'test'))) + .map(usableElm => new PomDependency(usableElm)) + .filter(pomDep => pomDep.isValid()) + .map(validPomDep => toDependency(validPomDep)); } - async collect(contents: string): Promise> { - this.parseXml(contents); - const deps = this.getXMLDependencies(this.xmlDocAst); - return this.mapToDependency(deps); + async collect(contents: string): Promise { + const xmlAst: XMLDocument = this.parseXml(contents); + const deps = this.getXMLDependencies(xmlAst); + return this.mapDependencies(deps); } - private getXMLDependencies(doc: XMLDocument): Array { - const validElementNames = ['groupId', 'artifactId']; - - return this.findRootNodes(doc, 'dependencies') - //must not be a dependency under dependencyManagement - .filter(e => { - const parentElement = e.parent as XMLElement | undefined; - return parentElement?.name !== 'dependencyManagement'; - }) - .map(node => node.subElements) - .flat(1) - .filter(e => e.name === 'dependency') - // must include all validElementNames - .filter(e => e.subElements.filter(elm => validElementNames.includes(elm.name)).length === validElementNames.length); - } } diff --git a/src/providers/requirements.txt.ts b/src/providers/requirements.txt.ts index e89871b8..50a56538 100644 --- a/src/providers/requirements.txt.ts +++ b/src/providers/requirements.txt.ts @@ -1,50 +1,47 @@ 'use strict'; -import { IKeyValueEntry, KeyValueEntry, Variant, ValueType, IDependency, IDependencyProvider, Dependency } from '../collector'; +import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; -class NaivePyParser { - constructor(contents: string) { - this.dependencies = NaivePyParser.parseDependencies(contents); +/* Process entries found in the txt files and collect all dependency + * related information */ +export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { + + constructor() { + super('pypi'); // set ecosystem to 'pypi' } - dependencies: Array; - - static parseDependencies(contents:string): Array { - const requirements = contents.split('\n'); - return requirements.reduce((dependencies, req, index) => { - // skip any text after # - if (req.includes('#')) { - req = req.split('#')[0]; - } - const parsedRequirement: Array = req.split(/[==,>=,<=]+/); - const pkgName:string = (parsedRequirement[0] || '').trim(); - // skip empty lines - if (pkgName.length > 0) { - const version = (parsedRequirement[1] || '').trim(); - const entry: IKeyValueEntry = new KeyValueEntry(pkgName.toLowerCase(), { line: 0, column: 0 }); - entry.value = new Variant(ValueType.String, version); - entry.valuePosition = { line: index + 1, column: req.indexOf(version) + 1 }; - dependencies.push(new Dependency(entry)); - } - return dependencies; - }, []); + private parseTxtDoc(contents: string): string[] { + return contents.split('\n'); } - parse(): Array { - return this.dependencies; + private parseLine(line: string, index: number): IDependency | null { + line = line.split('#')[0].trim(); // Remove comments + if (!line) return null; // Skip empty lines + + const lineData: string[] = line.split(/[==,>=,<=]+/); + if (lineData.length !== 2) return null; // Skip invalid lines + + const depName: string = lineData[0].trim().toLowerCase(); + const depVersion: string = lineData[1].trim(); + + return new Dependency( + { value: depName, position: { line: 0, column: 0 } }, + { value: depVersion, position: { line: index + 1, column: line.indexOf(depVersion) + 1 } }, + ); } -} -/* Process entries found in the txt files and collect all dependency - * related information */ -export class DependencyProvider implements IDependencyProvider { - ecosystem: string; - constructor(public classes: Array = ['dependencies']) { - this.ecosystem = 'pypi'; + private extractDependenciesFromLines(lines: string[]): IDependency[] { + return lines.reduce((dependencies: IDependency[], line: string, index: number) => { + const parsedDependency = this.parseLine(line, index); + if (parsedDependency) { + dependencies.push(parsedDependency); + } + return dependencies; + }, []); } - async collect(contents: string): Promise> { - const parser = new NaivePyParser(contents); - return parser.parse(); + async collect(contents: string): Promise { + const lines: string[] = this.parseTxtDoc(contents); + return this.extractDependenciesFromLines(lines); } } diff --git a/src/utils.ts b/src/utils.ts index 07865131..2169e1d6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,37 +4,25 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -import { Position, Range } from 'vscode-languageserver'; -import { IPositionedString, IPosition, IDependency } from './collector'; +import { Range } from 'vscode-languageserver'; +import { IPosition, IDependency } from './collector'; -/* VSCode and Che transmit the file buffer in a different manner, - * so we have to use different functions for computing the - * positions and ranges so that the lines are rendered properly. - */ - -const _toLspPositionChe = (pos: IPosition): Position => { - return {line: pos.line - 1, character: pos.column - 1}; -}; - -const _getRangeChe = (ps: IPositionedString): Range => { - const length = ps.value.length; - return { - start: _toLspPosition(ps.position), - end: {line: ps.position.line - 1, character: ps.position.column + length - 1} - }; -}; - -export const _toLspPosition = (pos: IPosition): Position => { - return _toLspPositionChe(pos); -}; - -export const getRange = (dep: IDependency): Range => { - if (dep.version.position.line !== 0) { - return _getRangeChe(dep.version); +export function getRange (dep: IDependency): Range { + const pos: IPosition = dep.version.position; + if (pos.line !== 0) { + const length = dep.version.value.length; + return { + start: { + line: pos.line - 1, + character: pos.column - 1 + }, + end: { + line: pos.line - 1, + character: pos.column + length - 1} + }; } else { return dep.context.range; } - }; export function isDefined(obj: any, ...keys: string[]): boolean { @@ -46,3 +34,11 @@ export function isDefined(obj: any, ...keys: string[]): boolean { } return true; } + +/* Please note :: There is an issue with the usage of semverRegex Node.js package in this code. + * Often times it fails to recognize versions that contain an added suffix, usually including extra details such as a timestamp and a commit hash. + * At the moment, using regex directly to extract versions inclusively. */ +export function semVerRegExp(str: string): RegExpExecArray { + const regExp = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/ig; + return regExp.exec(str); +} \ No newline at end of file diff --git a/src/vulnerability.ts b/src/vulnerability.ts index f16c135f..122062a1 100644 --- a/src/vulnerability.ts +++ b/src/vulnerability.ts @@ -23,7 +23,7 @@ class Vulnerability { const diagSeverity = DiagnosticSeverity.Error; - let msg: string = `${this.ref.replace(`pkg:${this.provider.ecosystem}/`, '')}`; + let msg: string = `${this.provider.resolveDependencyFromReference(this.ref)}`; this.dependencyData.forEach(dm => { msg += ` From 2e73ffed76cfbadfc046bfde192a5235c742ba51 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Wed, 15 Nov 2023 15:20:08 +0200 Subject: [PATCH 3/6] docs: add TypeDoc annotations --- package-lock.json | 100 ++++++++++++++++++++++++++++++ package.json | 1 + src/codeActionHandler.ts | 12 +++- src/collector.ts | 74 ++++++++++++++++++++-- src/componentAnalysis.ts | 32 ++++++++-- src/config.ts | 14 ++++- src/constants.ts | 8 ++- src/diagnosticsHandler.ts | 51 ++++++++++----- src/fileHandler.ts | 49 ++++++++++++++- src/providers/go.mod.ts | 81 ++++++++++++++++++++---- src/providers/package.json.ts | 30 +++++++-- src/providers/pom.xml.ts | 66 +++++++++++++++++--- src/providers/requirements.txt.ts | 34 ++++++++-- src/server.ts | 50 +++++++++++---- src/utils.ts | 35 ++--------- src/vulnerability.ts | 15 ++++- tsconfig.json | 6 +- 17 files changed, 554 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0de97a9..096b7ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.3", "typescript": "^5.2.2" } }, @@ -1363,6 +1364,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -3085,6 +3092,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3257,6 +3270,12 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3278,6 +3297,18 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4366,6 +4397,18 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -4686,6 +4729,51 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typedoc": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -4802,6 +4890,18 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index dfa658ae..2d17f631 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.3", "typescript": "^5.2.2" }, "scripts": { diff --git a/src/codeActionHandler.ts b/src/codeActionHandler.ts index 099f1a07..cedc55cb 100644 --- a/src/codeActionHandler.ts +++ b/src/codeActionHandler.ts @@ -9,6 +9,12 @@ import { codeActionsMap } from './diagnosticsHandler'; import { globalConfig } from './config'; import { RHDA_DIAGNOSTIC_SOURCE } from './constants'; +/** + * Retrieves code actions based on diagnostics and file type. + * @param diagnostics - An array of available diagnostics. + * @param fileType - The type of the file based on ecosystem (e.g., 'pom.xml'). + * @returns An array of CodeAction objects to be made available to the user. + */ function getDiagnosticsCodeActions( diagnostics: Diagnostic[], fileType: string ): CodeAction[] { const codeActions: CodeAction[] = []; let hasRhdaDiagonostic: boolean = false; @@ -18,7 +24,7 @@ function getDiagnosticsCodeActions( diagnostics: Diagnostic[], fileType: string if (codeAction) { if (fileType === 'pom.xml') { - // add RedHat repository recommendation command to action + // add Red Hat repository recommendation command to action codeAction.command = { title: 'RedHat repository recommendation', command: globalConfig.triggerRHRepositoryRecommendationNotification, @@ -38,6 +44,10 @@ function getDiagnosticsCodeActions( diagnostics: Diagnostic[], fileType: string return codeActions; } +/** + * Generates a code action for a detailed RHDA report on the analyzed manifest file. + * @returns A CodeAction object for an RHDA report. + */ function generateFullStackAnalysisAction(): CodeAction { return { title: 'Detailed Vulnerability Report', diff --git a/src/collector.ts b/src/collector.ts index d42a5099..8609b448 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -6,31 +6,42 @@ import { Range } from 'vscode-languageserver'; -/* Line and column inside the JSON file */ +/** + * Represents a position inside the manifest file with line and column information. + */ export interface IPosition { line: number; column: number; } -/* String value with position */ +/** + * Represents a string value with associated position information. + */ export interface IPositionedString { value: string; position: IPosition; } +/** + * Represents a context with a string value and its associated range. + */ export interface IPositionedContext { value: string; range: Range; } -/* Dependency specification */ +/** + * Represents a dependency specification. + */ export interface IDependency { name: IPositionedString; version: IPositionedString; context: IPositionedContext; } -/* Dependency class that can be created from `IKeyValueEntry` */ +/** + * Represents a dependency and implements the IDependency interface. + */ export class Dependency implements IDependency { constructor( public name: IPositionedString, @@ -39,23 +50,71 @@ export class Dependency implements IDependency { ) {} } +/** + * Retrieves the range of a dependency version or context within a text document. + * @param dep - The dependency object containing version and context information. + * @returns The range within the text document that represents the dependency. + */ +export function getRange (dep: IDependency): Range { + const pos: IPosition = dep.version.position; + if (pos.line !== 0) { + const length = dep.version.value.length; + return { + start: { + line: pos.line - 1, + character: pos.column - 1 + }, + end: { + line: pos.line - 1, + character: pos.column + length - 1} + }; + } else { + return dep.context.range; + } +} + +/** + * Represents a map of dependencies using dependency name as key for easy retrieval of associated details. + */ export class DependencyMap { mapper: Map; constructor(deps: IDependency[]) { this.mapper = new Map(deps.map(d => [d.name.value, d])); } + /** + * Retrieves a dependency by its unique name key. + * @param key - The unique name key for the desired dependency. + * @returns The dependency object linked to the specified unique name key. + */ public get(key: string): IDependency { return this.mapper.get(key); } } -/* Ecosystem provider interface */ +/** + * Represents an interface for providing ecosystem-specific dependencies. + */ export interface IDependencyProvider { + + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest contents to collect dependencies from. + * @returns A Promise resolving to an array of dependencies. + */ collect(contents: string): Promise; + + /** + * Resolves a dependency reference to its actual name in the ecosystem. + * @param ref - The reference string to resolve. + * @returns The resolved name of the dependency. + */ resolveDependencyFromReference(ref: string): string; } +/** + * Represents a resolver for ecosystem-specific dependencies. + */ export class EcosystemDependencyResolver { private ecosystem: string; @@ -63,6 +122,11 @@ export class EcosystemDependencyResolver { this.ecosystem = ecosystem; } + /** + * RResolves a dependency reference to its actual name in the specified ecosystem. + * @param ref - The reference string to resolve. + * @returns The resolved name of the dependency. + */ resolveDependencyFromReference(ref: string): string { return ref.replace(`pkg:${this.ecosystem}/`, ''); } diff --git a/src/componentAnalysis.ts b/src/componentAnalysis.ts index 2fd5d15d..ae0223b9 100644 --- a/src/componentAnalysis.ts +++ b/src/componentAnalysis.ts @@ -10,12 +10,17 @@ import { isDefined } from './utils'; import exhort from '@RHEcosystemAppEng/exhort-javascript-api'; -/* Source specification */ +/** + * Represents a source object with an ID and dependencies array. + */ interface ISource { id: string; dependencies: any[]; } +/** + * Implementation of ISource interface. + */ class Source implements ISource { constructor( public id: string, @@ -23,13 +28,18 @@ class Source implements ISource { ) {} } -/* Dependency Data specification */ +/** + * Represents data specification related to a dependency. + */ interface IDependencyData { sourceId: string; issuesCount: number; highestVulnerabilitySeverity: string; } +/** + * Implementation of IDependencyData interface. + */ class DependencyData implements IDependencyData { constructor( public sourceId: string, @@ -38,11 +48,16 @@ class DependencyData implements IDependencyData { ) {} } -/* Dependency Analysis Response specification */ +/** + * Represents the parsed response of Red Hat Dependency Analysis, with dependencies mapped by string keys. + */ interface IAnalysisResponse { dependencies: Map; } +/** + * Implementation of IAnalysisResponse interface. + */ class AnalysisResponse implements IAnalysisResponse { dependencies: Map = new Map(); @@ -83,9 +98,15 @@ class AnalysisResponse implements IAnalysisResponse { } } +/** + * Performs RHDA component analysis on provided manifest contents and fileType based on ecosystem. + * @param fileType - The type of file (e.g., 'pom.xml', 'package.json', 'go.mod', 'requirements.txt'). + * @param contents - The contents of the manifest file to analyze. + * @returns A Promise resolving to an AnalysisResponse object. + */ async function componentAnalysisService (fileType: string, contents: string): Promise { - // set up configuration options for the component analysis request + // Define configuration options for the component analysis request const options = { 'RHDA_TOKEN': globalConfig.telemetryId, 'RHDA_SOURCE': globalConfig.utmSource, @@ -103,8 +124,7 @@ async function componentAnalysisService (fileType: string, contents: string): Pr options['EXHORT_SNYK_TOKEN'] = globalConfig.exhortSnykToken; } - // get component analysis as JSON object - const componentAnalysisJson = await exhort.componentAnalysis(fileType, contents, options); + const componentAnalysisJson = await exhort.componentAnalysis(fileType, contents, options); // Execute component analysis return new AnalysisResponse(componentAnalysisJson); } diff --git a/src/config.ts b/src/config.ts index 2e0c2bb6..3796e709 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +5,9 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; +/** + * Represents the global configuration settings. + */ export class Config { triggerFullStackAnalysis: string; @@ -22,8 +25,10 @@ export class Config exhortPythonPath: string; exhortPipPath: string; + /** + * Initializes a new instance of the Config class with default values from the parent process environment variable data. + */ constructor() { - // init child process configuration with parent process environment data this.triggerFullStackAnalysis = process.env.VSCEXT_TRIGGER_FULL_STACK_ANALYSIS || ''; this.triggerRHRepositoryRecommendationNotification = process.env.VSCEXT_TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION || ''; this.telemetryId = process.env.VSCEXT_TELEMETRY_ID || ''; @@ -40,6 +45,10 @@ export class Config this.exhortPipPath = process.env.VSCEXT_EXHORT_PIP_PATH || 'pip'; } + /** + * Updates the global configuration with provided data from extension workspace settings. + * @param data - The data from extension workspace settings to update the global configuration with. + */ updateConfig( data: any ) { this.exhortSnykToken = data.redHatDependencyAnalytics.exhortSnykToken; this.matchManifestVersions = data.redHatDependencyAnalytics.matchManifestVersions ? 'true' : 'false'; @@ -53,6 +62,9 @@ export class Config } } +/** + * Represents the global configuration instance based on Config class. + */ const globalConfig = new Config(); export { globalConfig }; diff --git a/src/constants.ts b/src/constants.ts index 977ae3c7..7425dcb6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,7 +8,11 @@ * Commonly used constants */ -// RHDA source +/** + * Default source name for the Red Hat Dependency Analytics extension in diagnostics. + */ export const RHDA_DIAGNOSTIC_SOURCE = '\nRed Hat Dependency Analytics Plugin'; -// version placeholder for dependency template +/** + * Placeholder used as a version for dependency templates. + */ export const VERSION_PLACEHOLDER: string = '__VERSION__'; diff --git a/src/diagnosticsHandler.ts b/src/diagnosticsHandler.ts index cbb59b14..befc1f5b 100644 --- a/src/diagnosticsHandler.ts +++ b/src/diagnosticsHandler.ts @@ -8,17 +8,32 @@ import { Diagnostic, CodeAction } from 'vscode-languageserver'; import { DependencyMap, IDependencyProvider } from './collector'; import { componentAnalysisService, DependencyData } from './componentAnalysis'; import { Vulnerability } from './vulnerability'; -import { getRange } from './utils'; +import { getRange } from './collector'; import { connection } from './server'; import * as path from 'path'; -/* Diagnostics Pipeline specification */ +/** + * Diagnostics Pipeline specification. + */ interface IDiagnosticsPipeline { - clearDiagnostics(): void; - reportDiagnostics(): void; - runDiagnostics(dependencies: Map): void; + /** + * Clears diagnostics. + */ + clearDiagnostics(); + /** + * Reports diagnostics to the client. + */ + reportDiagnostics(); + /** + * Runs diagnostics on dependencies. + * @param dependencies - A map containing dependency data from exhort. + */ + runDiagnostics(dependencies: Map); } +/** + * Implementation of DiagnosticsPipeline interface. + */ class DiagnosticsPipeline implements IDiagnosticsPipeline { private diagnostics: Diagnostic[] = []; private vulnCount: number = 0; @@ -64,7 +79,7 @@ class DiagnosticsPipeline implements IDiagnosticsPipeline { const totalIssuesCount: number = dependencyData.reduce( (sum, currentItem) => sum + currentItem.issuesCount, - 0 // Initial value for the sum + 0 ); this.vulnCount += totalIssuesCount; } @@ -73,9 +88,16 @@ class DiagnosticsPipeline implements IDiagnosticsPipeline { } } +/** + * Performs diagnostics on the provided manifest file contents. + * @param diagnosticFilePath - The path to the manifest file. + * @param contents - The contents of the manifest file. + * @param provider - The dependency provider of the corresponding ecosystem. + * @returns A Promise that resolves when diagnostics are completed. + */ async function performDiagnostics(diagnosticFilePath: string, contents: string, provider: IDependencyProvider) { - - // collect dependencies from manifest + + // collect dependencies from manifest file let dependencies = null; dependencies = await provider.collect(contents) .catch(error => { @@ -86,17 +108,12 @@ async function performDiagnostics(diagnosticFilePath: string, contents: string, }); return; }); - - // map dependencies const dependencyMap = new DependencyMap(dependencies); - // init Diagnostics Pipeline const diagnosticsPipeline = new DiagnosticsPipeline(provider, dependencyMap, diagnosticFilePath); - // clear Diagnostics diagnosticsPipeline.clearDiagnostics(); - // execute Component Analysis const analysis = componentAnalysisService(path.basename(diagnosticFilePath), contents) .then(response => { diagnosticsPipeline.runDiagnostics(response.dependencies); @@ -110,10 +127,12 @@ async function performDiagnostics(diagnosticFilePath: string, contents: string, await analysis; - // report Diagnostics results diagnosticsPipeline.reportDiagnostics(); } -export const codeActionsMap = new Map(); +/** + * Map of code actions. + */ +const codeActionsMap = new Map(); -export { performDiagnostics }; \ No newline at end of file +export { performDiagnostics, codeActionsMap }; \ No newline at end of file diff --git a/src/fileHandler.ts b/src/fileHandler.ts index efc07174..e70c0384 100644 --- a/src/fileHandler.ts +++ b/src/fileHandler.ts @@ -13,23 +13,36 @@ import { DependencyProvider as PomXml } from './providers/pom.xml'; import { DependencyProvider as GoMod } from './providers/go.mod'; import { DependencyProvider as RequirementsTxt } from './providers/requirements.txt'; +/** + * Describes the available event streams. + */ enum EventStream { Invalid, Diagnostics } +/** + * Callback signature for file handling in analysis. + */ interface IAnalysisFileHandlerCallback { (uri: string, contents: string): void; } +/** + * Describes the structure of a file handler in analysis. + */ interface IAnalysisFileHandler { stream: EventStream; matcher: RegExp; callback: IAnalysisFileHandlerCallback; } +/** + * Implementation of a file handler in analysis. + */ class AnalysisFileHandler implements IAnalysisFileHandler { matcher: RegExp; + constructor( public stream: EventStream, matcher: string, @@ -39,13 +52,36 @@ class AnalysisFileHandler implements IAnalysisFileHandler { } } +/** + * Describes the collection of file handlers and file data for analysis. + */ interface IAnalysisFiles { handlers: IAnalysisFileHandler[]; fileData: Map; + + /** + * Assigns a file handler for a specific set of event stream, matcher and callback function. + * @param stream - The event stream to handle. + * @param matcher - The regular expression pattern to match filenames. + * @param cb - The callback function to handle the file. + * @returns IAnalysisFiles onject with an updated collection of file handlers. + */ on(stream: EventStream, matcher: string, cb: IAnalysisFileHandlerCallback): IAnalysisFiles; + + /** + * Executes file handler callback function based on given event stream and regular expression pattern match. + * @param stream - The event stream to execute handling. + * @param uri - The URI of the file. + * @param fileName - The base name of the file. + * @param contents - The contents of the file. + * @returns The result of file handling execution. + */ run(stream: EventStream, uri: string, file: string, contents: string): any; } +/** + * Implementation of a collection of file handlers and file data for analysis. + */ class AnalysisFiles implements IAnalysisFiles { constructor( public handlers: IAnalysisFileHandler[] = [], @@ -82,13 +118,24 @@ files.on(EventStream.Diagnostics, '^requirements\\.txt$', (uri, contents) => { performDiagnostics(uri, contents, new RequirementsTxt()); }); +/** + * Describes the LSP server for analysis. + */ interface IAnalysisLSPServer { conn: Connection; files: IAnalysisFiles; - + + /** + * Handles a file event in the LSP server. + * @param uri - The URI of the file. + * @param contents - The contents of the file. + */ handleFileEvent(uri: string, contents: string): void; } +/** + * Implementation of the LSP server for analysis. + */ class AnalysisLSPServer implements IAnalysisLSPServer { files: IAnalysisFiles = files; diff --git a/src/providers/go.mod.ts b/src/providers/go.mod.ts index 2fc0f99d..e3ee7a45 100644 --- a/src/providers/go.mod.ts +++ b/src/providers/go.mod.ts @@ -1,9 +1,28 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ 'use strict'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; -import { semVerRegExp } from '../utils' -/* Process entries found in the go.mod file */ +/* Please note :: There is an issue with the usage of semverRegex Node.js package in this code. + * Often times it fails to recognize versions that contain an added suffix, usually including extra details such as a timestamp and a commit hash. + * At the moment, using regex directly to extract versions inclusively. */ + +/** + * Executes a regular expression pattern match for Semantic Versioning (SemVer) within a given string. + * @param str - The string to search for a Semantic Versioning pattern. + * @returns An array of matched results for the Semantic Versioning pattern. + */ +export function semVerRegExp(str: string): RegExpExecArray { + const regExp = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/ig; + return regExp.exec(str); + } + +/** + * Process entries found in the go.mod file. + */ export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { replacementMap: Map = new Map(); @@ -11,35 +30,56 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I super('golang'); // set ecosystem to 'golang' } + /** + * Parses the provided string as an array of lines. + * @param contents - The string content to parse into lines. + * @returns An array of strings representing lines from the provided content. + */ static parseTxtDoc(contents: string): string[] { return contents.split('\n'); } + /** + * Cleans the given string by removing specific characters and words, + * such as 'require' and 'replace', parentheses, and consecutive spaces from the provided string. + * Additionally, trims any leading or trailing whitespace. + * @param line - The string to be cleaned. + * @returns The cleaned string. + */ static clean(line: string): string { return line.replace(/require|replace|\(|\)/g, '') .replace(/\s+/g, ' ') .trim(); } + /** + * Extracts dependency data from the provided line. + * @param line - The line to extract dependency data from. + * @returns An object containing dependency data, or null if no matching version is found. + */ static getDependencyData(line: string): { name: string, version: string, index: number } | null { const versionMatches: RegExpExecArray = semVerRegExp(line); if (versionMatches && versionMatches.length > 0) { const depName = DependencyProvider.clean(line).split(' ')[0]; - return {name: depName, version: versionMatches[0], index: versionMatches.index} + return {name: depName, version: versionMatches[0], index: versionMatches.index}; } return null; } + /** + * Registers a replacement dependency in the replacement map. + * @param line - The line containing the replacement statement. + * @param index - The index of the line in the file. + */ private registerReplacement(line: string, index: number) { - // split the replace statements by '=>' const lineData: string[] = line.split('=>'); - if (lineData.length !== 2) return; + if (lineData.length !== 2) { return; } let originalDepData = DependencyProvider.getDependencyData(lineData[0]); const replacementDepData = DependencyProvider.getDependencyData(lineData[1]); - if (!originalDepData) originalDepData = {name: DependencyProvider.clean(lineData[0]), version: null, index: null} - if (!replacementDepData) return; + if (!originalDepData) { originalDepData = {name: DependencyProvider.clean(lineData[0]), version: null, index: null}; } + if (!replacementDepData) { return; } const replaceDependency = new Dependency( { value: replacementDepData.name, position: { line: 0, column: 0 } }, @@ -49,10 +89,16 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I this.replacementMap.set(originalDepData.name + (originalDepData.version ? ('@v' + originalDepData.version) : ''), replaceDependency); } + /** + * Parses a line from the file and extracts dependency information. + * @param line - The line to parse for dependency information. + * @param index - The index of the line in the file. + * @returns An IDependency object representing the parsed dependency or null if no dependency is found. + */ private parseLine(line: string, index: number): IDependency | null { line = line.split('//')[0]; // Remove comments - if (!DependencyProvider.clean(line)) return null; // Skip lines without dependencies + if (!DependencyProvider.clean(line)) { return null; } // Skip lines without dependencies if (line.includes('=>')) { // stash replacement dependencies for replacement @@ -61,7 +107,7 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I } const depData = DependencyProvider.getDependencyData(line); - if (!depData) return null; + if (!depData) { return null; } return new Dependency( { value: depData.name, position: { line: 0, column: 0 } }, @@ -69,10 +115,20 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I ); } + /** + * Applies replacement dependency from replacement map to the provided dependency. + * @param dep - The dependency to be checked and replaced if necessary. + * @returns The replaced dependency or the original one if no replacement is found. + */ private applyReplaceMap(dep: IDependency): IDependency { return this.replacementMap.get(dep.name.value + '@' + dep.version.value) || this.replacementMap.get(dep.name.value) || dep; } + /** + * Extracts dependencies from lines parsed from the file. + * @param lines - An array of strings representing lines from the file. + * @returns An array of IDependency objects representing extracted dependencies. + */ private extractDependenciesFromLines(lines :string[]): IDependency[] { let isExcluded: boolean = false; const goModDeps: IDependency[] = lines.reduce((dependencies: IDependency[], line: string, index: number) => { @@ -91,7 +147,6 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I return dependencies; } - // parse included lines for dependencies const parsedDependency: IDependency = this.parseLine(line, index); if (parsedDependency) { dependencies.push(parsedDependency); @@ -101,10 +156,14 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I }, []); - // apply replacement dependencies return goModDeps.map(goModDep => this.applyReplaceMap(goModDep)); } + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest content to collect dependencies from. + * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. + */ async collect(contents: string): Promise { const lines: string[] = DependencyProvider.parseTxtDoc(contents); return this.extractDependenciesFromLines(lines); diff --git a/src/providers/package.json.ts b/src/providers/package.json.ts index 90872e0d..56268576 100644 --- a/src/providers/package.json.ts +++ b/src/providers/package.json.ts @@ -1,8 +1,15 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ 'use strict'; import jsonAst from 'json-to-ast'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; +/** + * Process entries found in the package.json file. + */ export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { private classes: string[] = ['dependencies']; @@ -10,12 +17,22 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I super('npm'); // set ecosystem to 'npm' } + /** + * Parses the provided manifest content into a JSON AST. + * @param contents - The manifest content to parse. + * @returns The parsed JSON AST. + */ private parseJson(contents: string): jsonAst { return jsonAst(contents || '{}'); } - private mapDependencies(jsonAst: jsonAst): IDependency[] { - return jsonAst.children + /** + * Maps dependencies from the parsed JSON AST to IDependency objects. + * @param jsonAst - The parsed JSON AST to map dependencies from. + * @returns An array of IDependency objects representing the dependencies. + */ + private mapDependencies(ast: jsonAst): IDependency[] { + return ast.children .filter(c => this.classes.includes(c.key.value)) .flatMap(c => c.value.children) .map(c => { @@ -26,10 +43,15 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I }); } + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest content to collect dependencies from. + * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. + */ async collect(contents: string): Promise { try { - const jsonAst: jsonAst = this.parseJson(contents); - return this.mapDependencies(jsonAst); + const ast: jsonAst = this.parseJson(contents); + return this.mapDependencies(ast); } catch (err) { if (err instanceof SyntaxError) { return []; diff --git a/src/providers/pom.xml.ts b/src/providers/pom.xml.ts index 0133cfa6..dd63c8b0 100644 --- a/src/providers/pom.xml.ts +++ b/src/providers/pom.xml.ts @@ -1,24 +1,42 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ 'use strict'; + import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; import { parse, DocumentCstNode } from '@xml-tools/parser'; import { buildAst, accept, XMLElement, XMLDocument } from '@xml-tools/ast'; import { VERSION_PLACEHOLDER } from '../constants'; +/** + * Process entries found in the pom.xml file. + */ export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { constructor() { super('maven'); // set ecosystem to 'maven' } + /** + * Parses the provided XML string into an XMLDocument AST. + * @param contents - The XML content to parse. + * @returns The parsed XMLDocument AST. + */ private parseXml(contents: string): XMLDocument { const { cst, tokenVector } = parse(contents); return buildAst(cst as DocumentCstNode, tokenVector); } + /** + * Retrieves the root elements of a specified root element within the given XMLDocument. + * @param document - The XMLDocument to search within. + * @param rootElementName - The name of the root element to search for. + * @returns An array of found XMLElements representing the root nodes. + */ private findRootNodes(document: XMLDocument, rootElementName: string): XMLElement[] { const properties: XMLElement[] = []; const propertiesElement = { - // Will be invoked once for each Element node in the AST. visitXMLElement: (node: XMLElement) => { if (node.name === rootElementName) { properties.push(node); @@ -29,41 +47,61 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I return properties; } + /** + * Retrieves XML dependencies from the provided XMLDocument. + * @param xmlAst - The XMLDocument AST to extract dependencies from. + * @returns An array of XMLElements representing XML dependencies. + */ private getXMLDependencies(xmlAst: XMLDocument): XMLElement[] { const validElementNames = ['groupId', 'artifactId']; return this.findRootNodes(xmlAst, 'dependencies') - //must not be a dependency under dependencyManagement .filter(e => { const parentElement = e.parent as XMLElement | undefined; return parentElement?.name !== 'dependencyManagement'; - }) + }) //must not be a dependency under dependencyManagement .map(node => node.subElements) .flat(1) .filter(e => e.name === 'dependency') - // must include all validElementNames - .filter(e => e.subElements.filter(elm => validElementNames.includes(elm.name)).length === validElementNames.length); + .filter(e => e.subElements.filter(elm => validElementNames.includes(elm.name)).length === validElementNames.length); // must include all validElementNames } + /** + * Maps XML dependencies to IDependency objects. + * @param deps - The XML dependencies to map. + * @returns An array of IDependency objects representing mapped dependencies. + */ private mapDependencies(deps: XMLElement[]): IDependency[] { + /** + * Define a class representing a dependency parsed from the pom.xml file + */ class PomDependency { public groupId: XMLElement; public artifactId: XMLElement; public version: XMLElement; + constructor(public element: XMLElement) { this.groupId = element.subElements.find(e => e.name === 'groupId'); this.artifactId = element.subElements.find(e => e.name === 'artifactId'); this.version = element.subElements.find(e => e.name === 'version'); } + /** + * Verifies the validity of the parsed dependency by ensuring the existence and non-emptiness of the groupId and artifactId elements. + * @returns A boolean indicating the validity of the parsed dependency. + */ isValid(): boolean { - // none should have a empty text. return [this.groupId, this.artifactId].find(e => !e.textContents[0]?.text) === undefined; } } + /** + * Converts a valid PomDependency into an IDependency object. + * @param d - A PomDependency instance. + * @returns An IDependency object derived from the PomDependency. + */ const toDependency = (d: PomDependency): Dependency => { const dep: Dependency = new Dependency( { value: `${d.groupId.textContents[0].text}/${d.artifactId.textContents[0].text}`, @@ -74,25 +112,30 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I start: { line: d.element.position.startLine - 1, character: d.element.position.startColumn - 1 }, end: { line: d.element.position.endLine - 1, character: d.element.position.endColumn } }, - } + }; if (d.version && d.version.textContents.length > 0) { const versionVal = d.version.textContents[0]; dep.version = { value: d.version.textContents[0].text, position: { line: versionVal.position.startLine, column: versionVal.position.startColumn }, - } + }; } else { dep.version = { value: '', position: { line: 0, column: 0 }, - } + }; dep.context.value = dependencyTemplate(d.element); } return dep; }; + /** + * Generates a dependency template for missing version information. + * @param dep - A XMLElement representing the dependency. + * @returns A string representing a dependency template with a placeholder for the version. + */ const dependencyTemplate = (dep: XMLElement): string => { let template = ''; let idx = 0; @@ -117,6 +160,11 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I .map(validPomDep => toDependency(validPomDep)); } + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest content to collect dependencies from. + * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. + */ async collect(contents: string): Promise { const xmlAst: XMLDocument = this.parseXml(contents); const deps = this.getXMLDependencies(xmlAst); diff --git a/src/providers/requirements.txt.ts b/src/providers/requirements.txt.ts index 50a56538..9b759226 100644 --- a/src/providers/requirements.txt.ts +++ b/src/providers/requirements.txt.ts @@ -1,25 +1,41 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat + * Licensed under the Apache-2.0 License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ 'use strict'; import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependency } from '../collector'; -/* Process entries found in the txt files and collect all dependency - * related information */ +/** + * Process entries found in the requirements.txt file. + */ export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { constructor() { super('pypi'); // set ecosystem to 'pypi' } + /** + * Parses the provided string as an array of lines. + * @param contents - The string content to parse into lines. + * @returns An array of strings representing lines from the provided content. + */ private parseTxtDoc(contents: string): string[] { return contents.split('\n'); } + /** + * Parses a line from the file and extracts dependency information. + * @param line - The line to parse for dependency information. + * @param index - The index of the line in the file. + * @returns An IDependency object representing the parsed dependency or null if no dependency is found. + */ private parseLine(line: string, index: number): IDependency | null { line = line.split('#')[0].trim(); // Remove comments - if (!line) return null; // Skip empty lines + if (!line) { return null; } // Skip empty lines const lineData: string[] = line.split(/[==,>=,<=]+/); - if (lineData.length !== 2) return null; // Skip invalid lines + if (lineData.length !== 2) { return null; } // Skip invalid lines const depName: string = lineData[0].trim().toLowerCase(); const depVersion: string = lineData[1].trim(); @@ -30,6 +46,11 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I ); } + /** + * Extracts dependencies from lines parsed from the file. + * @param lines - An array of strings representing lines from the file. + * @returns An array of IDependency objects representing extracted dependencies. + */ private extractDependenciesFromLines(lines: string[]): IDependency[] { return lines.reduce((dependencies: IDependency[], line: string, index: number) => { const parsedDependency = this.parseLine(line, index); @@ -40,6 +61,11 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I }, []); } + /** + * Collects dependencies from the provided manifest contents. + * @param contents - The manifest content to collect dependencies from. + * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. + */ async collect(contents: string): Promise { const lines: string[] = this.parseTxtDoc(contents); return this.extractDependenciesFromLines(lines); diff --git a/src/server.ts b/src/server.ts index 06d9b4ad..32139b9c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,15 +13,25 @@ import { globalConfig } from './config'; import { AnalysisLSPServer } from './fileHandler'; import { getDiagnosticsCodeActions } from './codeActionHandler'; -// declare timeout identifier to track delays for server.handleFileEvent execution +/** + * Declares timeout identifier to track delays for server.handleFileEvent execution + */ let checkDelay: NodeJS.Timeout; -// Create a connection for the server, using Node's IPC as a transport. +/** + * Represents the connection used for the server, using Node's IPC as a transport. + */ const connection: Connection = createConnection(ProposedFeatures.all); + +/** + * Represents the documents managed by the server. + */ const documents: TextDocuments = new TextDocuments(TextDocument); documents.listen(connection); -// Sets up the connection's initialization event handler. +/** + * Sets up the connection's initialization event handler. + */ let hasConfigurationCapability: boolean = false; connection.onInitialize((params): InitializeResult => { const capabilities = params.capabilities; @@ -36,30 +46,39 @@ connection.onInitialize((params): InitializeResult => { }; }); -// Registers a callback when the connection is fully initialized. +/** + * Registers a callback when the connection is fully initialized. + */ connection.onInitialized(() => { if (hasConfigurationCapability) { - // Register for all configuration changes. connection.client.register(DidChangeConfigurationNotification.type, undefined); } }); +/** + * Represents the server handling the Language Server Protocol requests and notifications. + */ const server = new AnalysisLSPServer(connection); -// triggered when document is opened +/** + * On open document event handler + */ connection.onDidOpenTextDocument((params) => { server.handleFileEvent(params.textDocument.uri, params.textDocument.text); }); -// triggered when document is saved +/** + * On save document event handler + */ connection.onDidSaveTextDocument((params) => { clearTimeout(checkDelay); server.handleFileEvent(params.textDocument.uri, server.files.fileData[params.textDocument.uri]); }); -// triggered when changes have been applied to document +/** + * On changes applied to document event handler + */ connection.onDidChangeTextDocument((params) => { - /* Update internal state for code lenses */ server.files.fileData[params.textDocument.uri] = params.contentChanges[0].text; clearTimeout(checkDelay); checkDelay = setTimeout(() => { @@ -67,23 +86,28 @@ connection.onDidChangeTextDocument((params) => { }, 3000); }); -// triggered when document is closed +/** + * On close document event handler + */ connection.onDidCloseTextDocument(() => { clearTimeout(checkDelay); }); -// Registering a callback when the configuration changes. +/** + * Registers a callback when the configuration changes. + */ connection.onDidChangeConfiguration(() => { if (hasConfigurationCapability) { - // Fetching the workspace configuration from the client. server.conn.workspace.getConfiguration() .then((data) => { - // Updating global settings based on the fetched configuration data. globalConfig.updateConfig(data); }); } }); +/** + * Handles code action requests from client. + */ connection.onCodeAction((params): CodeAction[] => { return getDiagnosticsCodeActions(params.context.diagnostics, path.basename(params.textDocument.uri)); }); diff --git a/src/utils.ts b/src/utils.ts index 2169e1d6..f667b37a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,27 +4,12 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -import { Range } from 'vscode-languageserver'; -import { IPosition, IDependency } from './collector'; - -export function getRange (dep: IDependency): Range { - const pos: IPosition = dep.version.position; - if (pos.line !== 0) { - const length = dep.version.value.length; - return { - start: { - line: pos.line - 1, - character: pos.column - 1 - }, - end: { - line: pos.line - 1, - character: pos.column + length - 1} - }; - } else { - return dep.context.range; - } -}; - +/** + * Checks if the specified keys are defined within the provided object. + * @param obj - The object to check for key definitions. + * @param keys - The keys to check for within the object. + * @returns A boolean indicating whether all specified keys are defined within the object. + */ export function isDefined(obj: any, ...keys: string[]): boolean { for (const key of keys) { if (!obj || !obj[key]) { @@ -33,12 +18,4 @@ export function isDefined(obj: any, ...keys: string[]): boolean { obj = obj[key]; } return true; -} - -/* Please note :: There is an issue with the usage of semverRegex Node.js package in this code. - * Often times it fails to recognize versions that contain an added suffix, usually including extra details such as a timestamp and a commit hash. - * At the moment, using regex directly to extract versions inclusively. */ -export function semVerRegExp(str: string): RegExpExecArray { - const regExp = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/ig; - return regExp.exec(str); } \ No newline at end of file diff --git a/src/vulnerability.ts b/src/vulnerability.ts index 122062a1..f4920c40 100644 --- a/src/vulnerability.ts +++ b/src/vulnerability.ts @@ -10,8 +10,17 @@ import { IDependencyProvider } from './collector'; import { DependencyData } from './componentAnalysis'; import { RHDA_DIAGNOSTIC_SOURCE } from './constants'; -/* Vulnerability data along with package name and version */ +/** + * Stores vulnerability data of a specific dependency. + */ class Vulnerability { + /** + * Creates a new instance of Vulnerability. + * @param provider - The dependency provider of the corresponding ecosystem. + * @param range - The text range within the document. + * @param ref - The reference name of the dependency. + * @param dependencyData - All vulnerability data regarding the dependency. + */ constructor( private provider: IDependencyProvider, private range: Range, @@ -19,6 +28,10 @@ class Vulnerability { private dependencyData: DependencyData[], ) {} + /** + * Creates a diagnostic object based on vulnerability data. + * @returns A Diagnostic object representing the vulnerability. + */ getDiagnostic(): Diagnostic { const diagSeverity = DiagnosticSeverity.Error; diff --git a/tsconfig.json b/tsconfig.json index e16be256..ca20d162 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,9 @@ "exclude": [ "node_modules", "test" - ] + ], + "typedocOptions": { + "entryPoints": ["src/**/*.ts"], + "out": "docs" + } } From 8a386ad0520d3305c9607a36cee0d7400d808983 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Sun, 19 Nov 2023 16:44:21 +0200 Subject: [PATCH 4/6] chore: remove unused make files Signed-off-by: Ilona Shishov --- make-artifacts.sh | 37 ------------------------------------- make-tar-che.sh | 6 ------ 2 files changed, 43 deletions(-) delete mode 100755 make-artifacts.sh delete mode 100755 make-tar-che.sh diff --git a/make-artifacts.sh b/make-artifacts.sh deleted file mode 100755 index 4d8ec924..00000000 --- a/make-artifacts.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -ex - -rm -Rf target/ output/ -npm install -npm run-script build -npm run dist - -# rename the tar to the version in the pom so it's easier to deploy it -if [[ $1 ]]; then mv ca-lsp-server{,-${1}}.tar; fi - -# move the tar into a target/ folder -mkdir -p target && mv *lsp-server*.tar* target/ - -# create a zip because downstream eclipse plugin build breaks when reading the tar -pushd output >/dev/null -if [[ ${1} ]]; then - zip -9r ../target/ca-lsp-server-${1}.zip * -else - zip -9r ../target/ca-lsp-server.zip * -fi -popd >/dev/null - -# to publish the generated tarball, pass in params: -# eg., $0 0.0.6-SNAPSHOT USER@SERVER:BASE/PATH 99 -if [[ $2 ]]; then - DESTINATION=$2 # set this to where you want to rsync the files, eg., USER@SERVER:BASE/PATH - SOURCEDIR=`pwd`/target - BUILD_TIMESTAMP=`date -u +%Y-%m-%d_%H-%M-%S` - if [[ $3 ]]; then BUILD_NUMBER="$3"; else BUILD_NUMBER=00; fi - for f in publish/rsync.sh util/cleanup/jbosstools-cleanup.sh; do - curl -s -S -k --create-dirs -o ${SOURCEDIR}/${f} https://raw.githubusercontent.com/jbosstools/jbosstools-build-ci/master/${f} && \ - chmod +x ${SOURCEDIR}/${f} - done - ${SOURCEDIR}/publish/rsync.sh -s ${SOURCEDIR} -i *lsp-server*.* \ - -DESTINATION ${DESTINATION} -k 4 -l 0 -a 4 --no-regen-metadata -BUILD_NUMBER ${BUILD_NUMBER} \ - -t photon/snapshots/builds/jbosstools-fabric8analytics-lsp-server_master/${BUILD_TIMESTAMP}-B${BUILD_NUMBER}/ -fi \ No newline at end of file diff --git a/make-tar-che.sh b/make-tar-che.sh deleted file mode 100755 index 28c7ed24..00000000 --- a/make-tar-che.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -ex - -rm -Rf ca-lsp-server.tar output/ -npm install -npm run-script build -npm run dist \ No newline at end of file From 466b6a8345a1f63644b5a9038e1fbf05fc7fffa2 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Tue, 21 Nov 2023 11:25:37 +0200 Subject: [PATCH 5/6] test: update unit tests Signed-off-by: Ilona Shishov --- package-lock.json | 170 +++++- package.json | 3 +- src/codeActionHandler.ts | 26 +- src/collector.ts | 15 +- src/componentAnalysis.ts | 2 +- src/config.ts | 2 - src/diagnosticsHandler.ts | 7 +- src/providers/go.mod.ts | 16 +- src/providers/package.json.ts | 16 +- src/providers/pom.xml.ts | 14 +- src/providers/requirements.txt.ts | 11 +- src/server.ts | 2 +- test/aggregators.test.ts | 91 --- test/codeActionHandler.test.ts | 78 +++ test/collector.test.ts | 76 ++- test/consumer.test.ts | 117 ---- test/providers/go.mod.test.ts | 729 ++++++++++------------ test/providers/package.json.test.ts | 240 +++++--- test/providers/pom.xml.test.ts | 782 ++++++++++++------------ test/providers/requirements.txt.test.ts | 114 ++-- test/vulnerability.test.ts | 206 +++---- 21 files changed, 1360 insertions(+), 1357 deletions(-) delete mode 100644 test/aggregators.test.ts create mode 100644 test/codeActionHandler.test.ts delete mode 100644 test/consumer.test.ts diff --git a/package-lock.json b/package-lock.json index 096b7ba4..6130f886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.7.1-ea.19", "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.0", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -31,6 +31,7 @@ "fake-exec": "^1.1.0", "mocha": "^10.2.0", "nyc": "^15.1.0", + "sinon": "^17.0.1", "ts-node": "^10.9.1", "typedoc": "^0.25.3", "typescript": "^5.2.2" @@ -38,7 +39,7 @@ }, "../exhort-javascript-api": { "name": "@RHEcosystemAppEng/exhort-javascript-api", - "version": "0.0.2-ea.49", + "version": "0.0.2-ea.50", "extraneous": true, "license": "Apache-2.0", "dependencies": { @@ -836,9 +837,9 @@ } }, "node_modules/@RHEcosystemAppEng/exhort-javascript-api": { - "version": "0.0.2-ea.49", - "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.0.2-ea.49/0380891b685a3eb30653010a6849669553ea4bb9", - "integrity": "sha512-APOe3QjMjE+Dx9ASZPN97Tpxq/fTvHic9IBTvfCeWhIK5M/WJ562B6U/YG7qjQmHfUur8jHXZOQpJ/bXfNBKDA==", + "version": "0.1.0", + "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.0/3159c652ef143fbd27ea8decb881d0f460384cdf", + "integrity": "sha512-YstZq1eAUYitnu5DKP3jcfug93B0EAmsK88FAgLQFp5fmFSaCnpnbkyeKpLzulGC4T5bgnq/t1/QswkAWtdoLg==", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.23.2", @@ -855,6 +856,50 @@ "npm": ">= 9.0.0" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2854,6 +2899,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3098,6 +3149,12 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3161,6 +3218,12 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3584,6 +3647,46 @@ "url": "https://nearley.js.org/#give-to-nearley" } }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -4009,6 +4112,15 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4415,6 +4527,54 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "devOptional": true }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 2d17f631..ed181c10 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dist" ], "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.0.2-ea.49", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.0", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -43,6 +43,7 @@ "fake-exec": "^1.1.0", "mocha": "^10.2.0", "nyc": "^15.1.0", + "sinon": "^17.0.1", "ts-node": "^10.9.1", "typedoc": "^0.25.3", "typescript": "^5.2.2" diff --git a/src/codeActionHandler.ts b/src/codeActionHandler.ts index cedc55cb..4c7c91b5 100644 --- a/src/codeActionHandler.ts +++ b/src/codeActionHandler.ts @@ -5,42 +5,22 @@ 'use strict'; import { CodeAction, CodeActionKind, Diagnostic } from 'vscode-languageserver/node'; -import { codeActionsMap } from './diagnosticsHandler'; import { globalConfig } from './config'; import { RHDA_DIAGNOSTIC_SOURCE } from './constants'; /** * Retrieves code actions based on diagnostics and file type. * @param diagnostics - An array of available diagnostics. - * @param fileType - The type of the file based on ecosystem (e.g., 'pom.xml'). * @returns An array of CodeAction objects to be made available to the user. */ -function getDiagnosticsCodeActions( diagnostics: Diagnostic[], fileType: string ): CodeAction[] { +function getDiagnosticsCodeActions(diagnostics: Diagnostic[]): CodeAction[] { + const hasRhdaDiagonostic = diagnostics.some(diagnostic => diagnostic.source === RHDA_DIAGNOSTIC_SOURCE); const codeActions: CodeAction[] = []; - let hasRhdaDiagonostic: boolean = false; - - for (const diagnostic of diagnostics) { - const codeAction = codeActionsMap[diagnostic.range.start.line + '|' + diagnostic.range.start.character]; - if (codeAction) { - - if (fileType === 'pom.xml') { - // add Red Hat repository recommendation command to action - codeAction.command = { - title: 'RedHat repository recommendation', - command: globalConfig.triggerRHRepositoryRecommendationNotification, - }; - } - codeActions.push(codeAction); - - } - if (!hasRhdaDiagonostic) { - hasRhdaDiagonostic = diagnostic.source === RHDA_DIAGNOSTIC_SOURCE; - } - } if (globalConfig.triggerFullStackAnalysis && hasRhdaDiagonostic) { codeActions.push(generateFullStackAnalysisAction()); } + return codeActions; } diff --git a/src/collector.ts b/src/collector.ts index 8609b448..b1121ef0 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -5,6 +5,7 @@ 'use strict'; import { Range } from 'vscode-languageserver'; +import { isDefined } from './utils'; /** * Represents a position inside the manifest file with line and column information. @@ -43,10 +44,11 @@ export interface IDependency { * Represents a dependency and implements the IDependency interface. */ export class Dependency implements IDependency { + public version: IPositionedString + public context: IPositionedContext + constructor( - public name: IPositionedString, - public version: IPositionedString = {} as IPositionedString, - public context: IPositionedContext = {} as IPositionedContext, + public name: IPositionedString ) {} } @@ -56,8 +58,9 @@ export class Dependency implements IDependency { * @returns The range within the text document that represents the dependency. */ export function getRange (dep: IDependency): Range { - const pos: IPosition = dep.version.position; - if (pos.line !== 0) { + + if (isDefined(dep, 'version', 'position')) { + const pos: IPosition = dep.version.position; const length = dep.version.value.length; return { start: { @@ -123,7 +126,7 @@ export class EcosystemDependencyResolver { } /** - * RResolves a dependency reference to its actual name in the specified ecosystem. + * Resolves a dependency reference in a specified ecosystem to its name and version string. * @param ref - The reference string to resolve. * @returns The resolved name of the dependency. */ diff --git a/src/componentAnalysis.ts b/src/componentAnalysis.ts index ae0223b9..b81ae5f0 100644 --- a/src/componentAnalysis.ts +++ b/src/componentAnalysis.ts @@ -86,7 +86,7 @@ class AnalysisResponse implements IAnalysisResponse { sources.forEach(source => { source.dependencies.forEach(d => { if (isDefined(d, 'ref') && isDefined(d, 'issues')) { - const dd = new DependencyData(source.id, d.issues.length, isDefined(d, 'highestVulnerability', 'severity') ? d.highestVulnerability.severity : 'UNKNOWN'); + const dd = new DependencyData(source.id, d.issues.length, isDefined(d, 'highestVulnerability', 'severity') ? d.highestVulnerability.severity : 'NONE'); if (this.dependencies[d.ref] === undefined) { this.dependencies[d.ref] = []; } diff --git a/src/config.ts b/src/config.ts index 3796e709..9617678a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,7 +11,6 @@ export class Config { triggerFullStackAnalysis: string; - triggerRHRepositoryRecommendationNotification: string; telemetryId: string; utmSource: string; exhortDevMode: string; @@ -30,7 +29,6 @@ export class Config */ constructor() { this.triggerFullStackAnalysis = process.env.VSCEXT_TRIGGER_FULL_STACK_ANALYSIS || ''; - this.triggerRHRepositoryRecommendationNotification = process.env.VSCEXT_TRIGGER_REDHAT_REPOSITORY_RECOMMENDATION_NOTIFICATION || ''; this.telemetryId = process.env.VSCEXT_TELEMETRY_ID || ''; this.utmSource = process.env.VSCEXT_UTM_SOURCE || ''; this.exhortDevMode = process.env.VSCEXT_EXHORT_DEV_MODE || 'false'; diff --git a/src/diagnosticsHandler.ts b/src/diagnosticsHandler.ts index befc1f5b..cef96b6c 100644 --- a/src/diagnosticsHandler.ts +++ b/src/diagnosticsHandler.ts @@ -130,9 +130,4 @@ async function performDiagnostics(diagnosticFilePath: string, contents: string, diagnosticsPipeline.reportDiagnostics(); } -/** - * Map of code actions. - */ -const codeActionsMap = new Map(); - -export { performDiagnostics, codeActionsMap }; \ No newline at end of file +export { performDiagnostics }; \ No newline at end of file diff --git a/src/providers/go.mod.ts b/src/providers/go.mod.ts index e3ee7a45..83f3fe2c 100644 --- a/src/providers/go.mod.ts +++ b/src/providers/go.mod.ts @@ -73,7 +73,6 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I */ private registerReplacement(line: string, index: number) { const lineData: string[] = line.split('=>'); - if (lineData.length !== 2) { return; } let originalDepData = DependencyProvider.getDependencyData(lineData[0]); const replacementDepData = DependencyProvider.getDependencyData(lineData[1]); @@ -81,11 +80,9 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I if (!originalDepData) { originalDepData = {name: DependencyProvider.clean(lineData[0]), version: null, index: null}; } if (!replacementDepData) { return; } - const replaceDependency = new Dependency( - { value: replacementDepData.name, position: { line: 0, column: 0 } }, - { value: 'v' + replacementDepData.version, position: { line: index + 1, column: (line.lastIndexOf(lineData[1]) + replacementDepData.index) } }, - ); - + const replaceDependency = new Dependency({ value: replacementDepData.name, position: { line: 0, column: 0 } }); + replaceDependency.version = { value: 'v' + replacementDepData.version, position: { line: index + 1, column: (line.lastIndexOf(lineData[1]) + replacementDepData.index) } }; + this.replacementMap.set(originalDepData.name + (originalDepData.version ? ('@v' + originalDepData.version) : ''), replaceDependency); } @@ -109,10 +106,9 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I const depData = DependencyProvider.getDependencyData(line); if (!depData) { return null; } - return new Dependency( - { value: depData.name, position: { line: 0, column: 0 } }, - { value: 'v' + depData.version, position: { line: index + 1, column: depData.index } }, - ); + const dep = new Dependency({ value: depData.name, position: { line: 0, column: 0 } }); + dep.version = { value: 'v' + depData.version, position: { line: index + 1, column: depData.index } }; + return dep; } /** diff --git a/src/providers/package.json.ts b/src/providers/package.json.ts index 56268576..5bbf69eb 100644 --- a/src/providers/package.json.ts +++ b/src/providers/package.json.ts @@ -11,7 +11,7 @@ import { IDependencyProvider, EcosystemDependencyResolver, IDependency, Dependen * Process entries found in the package.json file. */ export class DependencyProvider extends EcosystemDependencyResolver implements IDependencyProvider { - private classes: string[] = ['dependencies']; + classes: string[] = ['dependencies']; constructor() { super('npm'); // set ecosystem to 'npm' @@ -36,10 +36,9 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I .filter(c => this.classes.includes(c.key.value)) .flatMap(c => c.value.children) .map(c => { - return new Dependency( - { value: c.key.value, position: {line: c.key.loc.start.line, column: c.key.loc.start.column + 1} }, - { value: c.value.value, position: {line: c.value.loc.start.line, column: c.value.loc.start.column + 1} }, - ); + const dep = new Dependency({ value: c.key.value, position: {line: c.key.loc.start.line, column: c.key.loc.start.column + 1} }); + dep.version = { value: c.value.value, position: {line: c.value.loc.start.line, column: c.value.loc.start.column + 1} }; + return dep; }); } @@ -49,14 +48,17 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I * @returns A Promise resolving to an array of IDependency objects representing collected dependencies. */ async collect(contents: string): Promise { + let ast: jsonAst; + try { - const ast: jsonAst = this.parseJson(contents); - return this.mapDependencies(ast); + ast = this.parseJson(contents); } catch (err) { if (err instanceof SyntaxError) { return []; } throw err; } + + return this.mapDependencies(ast); } } diff --git a/src/providers/pom.xml.ts b/src/providers/pom.xml.ts index dd63c8b0..b188c319 100644 --- a/src/providers/pom.xml.ts +++ b/src/providers/pom.xml.ts @@ -107,12 +107,6 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I { value: `${d.groupId.textContents[0].text}/${d.artifactId.textContents[0].text}`, position: { line: d.element.position.startLine, column: d.element.position.startColumn } }, ); - - dep.context = { value: '', range: { - start: { line: d.element.position.startLine - 1, character: d.element.position.startColumn - 1 }, - end: { line: d.element.position.endLine - 1, character: d.element.position.endColumn } - }, - }; if (d.version && d.version.textContents.length > 0) { const versionVal = d.version.textContents[0]; @@ -121,11 +115,11 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I position: { line: versionVal.position.startLine, column: versionVal.position.startColumn }, }; } else { - dep.version = { - value: '', - position: { line: 0, column: 0 }, + dep.context = { value: dependencyTemplate(d.element), range: { + start: { line: d.element.position.startLine - 1, character: d.element.position.startColumn - 1 }, + end: { line: d.element.position.endLine - 1, character: d.element.position.endColumn } + }, }; - dep.context.value = dependencyTemplate(d.element); } return dep; diff --git a/src/providers/requirements.txt.ts b/src/providers/requirements.txt.ts index 9b759226..810a645a 100644 --- a/src/providers/requirements.txt.ts +++ b/src/providers/requirements.txt.ts @@ -31,8 +31,8 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I * @returns An IDependency object representing the parsed dependency or null if no dependency is found. */ private parseLine(line: string, index: number): IDependency | null { - line = line.split('#')[0].trim(); // Remove comments - if (!line) { return null; } // Skip empty lines + line = line.split('#')[0]; // Remove comments + if (!line.trim()) { return null; } // Skip empty lines const lineData: string[] = line.split(/[==,>=,<=]+/); if (lineData.length !== 2) { return null; } // Skip invalid lines @@ -40,10 +40,9 @@ export class DependencyProvider extends EcosystemDependencyResolver implements I const depName: string = lineData[0].trim().toLowerCase(); const depVersion: string = lineData[1].trim(); - return new Dependency( - { value: depName, position: { line: 0, column: 0 } }, - { value: depVersion, position: { line: index + 1, column: line.indexOf(depVersion) + 1 } }, - ); + const dep = new Dependency ({ value: depName, position: { line: 0, column: 0 } }); + dep.version = { value: depVersion, position: { line: index + 1, column: line.indexOf(depVersion) + 1 } }; + return dep; } /** diff --git a/src/server.ts b/src/server.ts index 32139b9c..fe99e1c4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -109,7 +109,7 @@ connection.onDidChangeConfiguration(() => { * Handles code action requests from client. */ connection.onCodeAction((params): CodeAction[] => { - return getDiagnosticsCodeActions(params.context.diagnostics, path.basename(params.textDocument.uri)); + return getDiagnosticsCodeActions(params.context.diagnostics); }); connection.listen(); diff --git a/test/aggregators.test.ts b/test/aggregators.test.ts deleted file mode 100644 index 466deb55..00000000 --- a/test/aggregators.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { expect } from 'chai'; -import { Range } from 'vscode-languageserver'; -import { DependencyProvider as PackageJson } from '../src/providers/package.json'; -import { DependencyProvider as PomXml } from '../src/providers/pom.xml'; -import { NoopVulnerabilityAggregator, MavenVulnerabilityAggregator } from '../src/aggregators'; -import { Vulnerability, ANALYTICS_SOURCE } from '../src/vulnerability'; - -const dummyRange: Range = { - start: { - line: 3, - character: 4 - }, - end: { - line: 3, - character: 10 - } -} - -describe('Noop vulnerability aggregator tests', () => { - - it('Test Noop aggregator', async () => { - let noopVulnerabilityAggregator = new NoopVulnerabilityAggregator(new PackageJson()); - // let vulnerability = new Vulnerability(dummyRange, 0, 'pkg:npm/MockPkg@1.2.3', null, '', '', null, ''); - let vulnerability = new Vulnerability(dummyRange, 'pkg:npm/MockPkg@1.2.3', 0, ''); - let aggVulnerability = noopVulnerabilityAggregator.aggregate(vulnerability); - - // const msg = 'pkg:npm/MockPkg@1.2.3\nRecommendation: No RedHat packages to recommend'; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Information, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - expect(noopVulnerabilityAggregator.isNewVulnerability).to.equal(true); - expect(aggVulnerability.provider.ecosystem).is.eql('npm') - // expect(aggVulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - }); -}); - -describe('Maven vulnerability aggregator tests', () => { - - it('Test Maven aggregator with one vulnerability', async () => { - let mavenVulnerabilityAggregator = new MavenVulnerabilityAggregator(new PomXml()); - // let vulnerability = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, null, '', '', null, ''); - let vulnerability = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, ''); - let aggVulnerability = mavenVulnerabilityAggregator.aggregate(vulnerability); - - // const msg = 'pkg:maven/MockPkg@1.2.3\nRecommendation: No RedHat packages to recommend'; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Information, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - expect(mavenVulnerabilityAggregator.isNewVulnerability).to.equal(true); - expect(aggVulnerability.provider.ecosystem).is.eql('maven') - // expect(aggVulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - }); - - it('Test Maven aggregator with two identical vulnerability', async () => { - let mavenVulnerabilityAggregator = new MavenVulnerabilityAggregator(new PomXml()); - // let vulnerability1 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, null, '', '', null, ''); - let vulnerability1 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, ''); - let aggVulnerability1 = mavenVulnerabilityAggregator.aggregate(vulnerability1); - // let vulnerability2 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, null, '', '', null, ''); - let vulnerability2 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg@1.2.3', 0, ''); - let aggVulnerability2 = mavenVulnerabilityAggregator.aggregate(vulnerability2); - - expect(mavenVulnerabilityAggregator.isNewVulnerability).to.equal(false); - expect(mavenVulnerabilityAggregator.vulnerabilities.size).to.equal(1); - expect(aggVulnerability1.provider.ecosystem).is.eql('maven') - expect(aggVulnerability2.provider.ecosystem).is.eql('maven') - }); - - it('Test Maven aggregator with two different vulnerability', async () => { - let mavenVulnerabilityAggregator = new MavenVulnerabilityAggregator(new PomXml()); - // let vulnerability1 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg1@1.2.3', 0, null, '', '', null, ''); - let vulnerability1 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg1@1.2.3', 0, ''); - let aggVulnerability1 = mavenVulnerabilityAggregator.aggregate(vulnerability1); - // let vulnerability2 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg2@1.2.3', 0, null, '', '', null, ''); - let vulnerability2 = new Vulnerability(dummyRange, 'pkg:maven/MockPkg2@1.2.3', 0, ''); - let aggVulnerability2 = mavenVulnerabilityAggregator.aggregate(vulnerability2); - - expect(mavenVulnerabilityAggregator.isNewVulnerability).to.equal(true); - expect(mavenVulnerabilityAggregator.vulnerabilities.size).to.equal(2); - expect(aggVulnerability1.provider.ecosystem).is.eql('maven') - expect(aggVulnerability2.provider.ecosystem).is.eql('maven') - }); -}); \ No newline at end of file diff --git a/test/codeActionHandler.test.ts b/test/codeActionHandler.test.ts new file mode 100644 index 00000000..3267d378 --- /dev/null +++ b/test/codeActionHandler.test.ts @@ -0,0 +1,78 @@ +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Range } from 'vscode-languageserver'; +import { CodeAction, CodeActionKind, Diagnostic } from 'vscode-languageserver/node'; +import * as config from '../src/config'; +import { RHDA_DIAGNOSTIC_SOURCE } from '../src/constants'; +import { getDiagnosticsCodeActions } from '../src/codeActionHandler'; + +describe('Code Action Handler test', () => { + + const mockRange: Range = { + start: { + line: 123, + character: 123 + }, + end: { + line: 456, + character: 456 + } + }; + + const mockDiagnostics: Diagnostic[] = [ + { + severity: 1, + range: mockRange, + message: 'mock message', + source: RHDA_DIAGNOSTIC_SOURCE, + }, + { + severity: 2, + range: mockRange, + message: 'another mock message', + source: RHDA_DIAGNOSTIC_SOURCE, + } + ]; + + it('should return an empty array if no RHDA diagnostics are present', () => { + const diagnostics: Diagnostic[] = []; + + const codeActions = getDiagnosticsCodeActions(diagnostics); + + expect(codeActions).to.be.an('array').that.is.empty; + }); + + it('should generate code actions for RHDA diagnostics when full stack analysis action is provided', async () => { + let globalConfig = { + triggerFullStackAnalysis: 'mockAction' + }; + sinon.stub(config, 'globalConfig').value(globalConfig); + + const codeActions: CodeAction[] = getDiagnosticsCodeActions(mockDiagnostics); + + expect(codeActions).to.deep.equal( + [ + { + title: 'Detailed Vulnerability Report', + kind: CodeActionKind.QuickFix, + command: { + title: 'Analytics Report', + command: 'mockAction' } + } + ] + ); + }); + + it('should return an empty array when no full stack analysis action is provided', async () => { + let globalConfig = { + triggerFullStackAnalysis: '' + }; + sinon.stub(config, 'globalConfig').value(globalConfig); + + const codeActions: CodeAction[] = getDiagnosticsCodeActions(mockDiagnostics); + + expect(codeActions).to.be.an('array').that.is.empty; + }); +}); \ No newline at end of file diff --git a/test/collector.test.ts b/test/collector.test.ts index ddc0cb7d..93abb9c4 100644 --- a/test/collector.test.ts +++ b/test/collector.test.ts @@ -1,31 +1,59 @@ 'use strict'; import { expect } from 'chai'; -import { Dependency, IPosition, ValueType, Variant, KeyValueEntry, DependencyMap } from '../src/collector'; +import { Dependency, DependencyMap, getRange } from '../src/collector'; +import { DependencyProvider } from '../src/providers/pom.xml'; describe('Collector util test', () => { - const pos: IPosition = { - line: 123, - column: 123 - }; - - // define manifest dependencies - const reqDeps: Array = [ - new Dependency(new KeyValueEntry('mockGtoup1/mockArtifact1', pos, new Variant(ValueType.String, 'mockVersion1'), pos)), - new Dependency(new KeyValueEntry('mockGtoup2/mockArtifact2', pos, new Variant(ValueType.String, 'mockVersion2'), pos)) - ]; - - // define api response dependencies - const resDeps: any[] = [ - { ref: 'pkg:maven/mockGtoup1/mockArtifact1@mockVersion1' }, - { ref: 'pkg:maven/mockGtoup2/mockArtifact2@mockVersion2' } - ]; - - it('create map for maven dependecies', async () => { - const ecosystem = 'maven'; - const map = new DependencyMap(reqDeps); - expect(map.get(resDeps[0].ref.split('@')[0].replace(`pkg:${ecosystem}/`, ''))).to.eql(reqDeps[0]); - expect(map.get(resDeps[1].ref.split('@')[0].replace(`pkg:${ecosystem}/`, ''))).to.eql(reqDeps[1]); - }); + // Mock manifest dependency collection + const reqDeps: Dependency[] = [ + new Dependency ({ value: 'mockGroupId1/mockArtifact1', position: { line: 0, column: 0 } }), + new Dependency ({ value: 'mockGroupId2/mockArtifact2', position: { line: 0, column: 0 } }) + ]; + + it('should create map of dependecies', async () => { + + const depMap = new DependencyMap(reqDeps); + + expect(Object.fromEntries(depMap.mapper)).to.eql({ + 'mockGroupId1/mockArtifact1': reqDeps[0], + 'mockGroupId2/mockArtifact2': reqDeps[1] + }); + }); + + it('should get dependency from dependency map', async () => { + + const depMap = new DependencyMap(reqDeps); + + expect(depMap.get(reqDeps[0].name.value)).to.eq(reqDeps[0]); + expect(depMap.get(reqDeps[1].name.value)).to.eq(reqDeps[1]); + }); + + it('should return dependency range', async () => { + + reqDeps[0].version = { value: 'mockVersion', position: { line: 123, column: 123 } }; + reqDeps[1].context = { value: 'mockRange', range: { + start: { line: 123, character: 123 }, + end: { line: 456, character: 456 } + }, + }; + + expect(getRange(reqDeps[0])).to.eql( + { + start: { line: 122, character: 122 }, + end: { line: 122, character: 133 } + } + ); + + expect(getRange(reqDeps[1])).to.eql(reqDeps[1].context.range); + }); + + it('should resolves a dependency reference in a specified ecosystem to its name and version string', async () => { + const mavenDependencyProvider = new DependencyProvider(); + + const resolvedRef = mavenDependencyProvider.resolveDependencyFromReference('pkg:maven/mockGroupId1/mockArtifact1@mockVersion1'); + + expect(resolvedRef).to.eq('mockGroupId1/mockArtifact1@mockVersion1'); + }); }) diff --git a/test/consumer.test.ts b/test/consumer.test.ts deleted file mode 100644 index 93162992..00000000 --- a/test/consumer.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { expect } from 'chai'; -import { DependencyProvider as PackageJson } from '../src/providers/package.json'; -import { SecurityEngine, DiagnosticsPipeline } from '../src/consumers'; -import { NoopVulnerabilityAggregator } from '../src/aggregators'; -import { Diagnostic } from 'vscode-languageserver'; - -const config = {}; -const diagnosticFilePath = "path/to/mock/diagnostic"; -const pkg = { - name: { - value: "MockPkg", - position: { - line: 20, - column: 6 - } - }, - version: { - value: "1.2.3", - position: { - line: 20, - column: 17 - } - }, - context: { - value: "MockPkg1.2.3", - range: { - start: { - line: 20, - character: 6 - }, - end: { - line: 20, - character:17 - } - } - } -}; - -describe('Response consumer test', () => { - it('Consume response without vulnerabilities', () => { - let diagnostics: Diagnostic[] = []; - let packageAggregator = new NoopVulnerabilityAggregator(new PackageJson()); - const dependency = { - ref: "pkg:npm/MockPkg@1.2.3", - issues: [ - ], - transitive: [ - ], - recommendation: { - name: 'mockRecommendationName', - version: 'mockRecommendationVersion', - }, - remediations: { - }, - highestVulnerability: { - }, - } - - let pipeline = new DiagnosticsPipeline(SecurityEngine, pkg, config, diagnostics, packageAggregator, diagnosticFilePath); - pipeline.run(dependency); - const secEng = pipeline.item as SecurityEngine; - // const msg = 'MockPkg@1.2.3\nRecommendation: mockRecommendationName:mockRecommendationVersion'; - - // expect(diagnostics.length).equal(1); - expect(diagnostics.length).equal(0); - expect(secEng.issuesCount).equal(0); - // expect(diagnostics[0].message.toString().replace(/\s/g, "")).equal(msg.toString().replace(/\s/g, "")); - }); - - it('Consume response with vulnerabilities', () => { - let diagnostics: Diagnostic[] = []; - let packageAggregator = new NoopVulnerabilityAggregator(new PackageJson()); - const dependency = { - ref: "pkg:npm/MockPkg@1.2.3", - issues: [ - { - id: "MockIssue", - }, - ], - transitive: [ - ], - recommendation: null, - remediations: { - "mockCVE": { - npmPackage: "pkg:npm/MockPkg@4.5.6", - }, - }, - highestVulnerability: { - id: "MockIssue", - severity: "MockSeverity", - }, - } - - let pipeline = new DiagnosticsPipeline(SecurityEngine, pkg, config, diagnostics, packageAggregator, diagnosticFilePath); - pipeline.run(dependency); - const secEng = pipeline.item as SecurityEngine; - // const msg = "MockPkg@1.2.3\nKnown security vulnerabilities: 1\nHighest severity: MockSeverity\nHas remediation: Yes"; - const msg = "MockPkg@1.2.3\nKnown security vulnerabilities: 1\nHighest severity: MockSeverity"; - - expect(diagnostics.length).equal(1); - expect(secEng.issuesCount).equal(1); - expect(diagnostics[0].message.toString().replace(/\s/g, "")).equal(msg.toString().replace(/\s/g, "")); - }); - - it('Consume invalid response', () => { - let diagnostics: Diagnostic[] = []; - let packageAggregator = new NoopVulnerabilityAggregator(new PackageJson()); - const dependency = { - ref: "pkg:npm/MockPkg@1.2.3", - }; - - let pipeline = new DiagnosticsPipeline(SecurityEngine, pkg, config, diagnostics, packageAggregator, diagnosticFilePath); - pipeline.run(dependency); - - expect(diagnostics.length).equal(0); - }); -}); diff --git a/test/providers/go.mod.test.ts b/test/providers/go.mod.test.ts index 7a4fe02a..a5ab450d 100644 --- a/test/providers/go.mod.test.ts +++ b/test/providers/go.mod.test.ts @@ -1,434 +1,347 @@ +'use strict'; + import { expect } from 'chai'; import { DependencyProvider } from '../../src/providers/go.mod'; -const fake = require('fake-exec'); - -describe('Golang go.mod parser test', () => { - const provider = new DependencyProvider(); - - it('tests valid go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - require ( - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 - github.com/stretchr/testify v1.2.2 - ) - go 1.13 - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 4, column: 41 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.1', position: { line: 5, column: 40 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.0.0', position: { line: 6, column: 43 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.2', position: { line: 7, column: 41 } } - }); - }); - - it('tests go.mod with comments', async () => { - const deps = await provider.collect(`// This is start point. - module github.com/alecthomas/kingpin - require ( - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // Valid data before this. - // Extra comment in require section. - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 - ) - go 1.13 - // Final notes. - `); - expect(deps.length).equal(3); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 4, column: 41 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.0.0', position: { line: 6, column: 43 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.2', position: { line: 7, column: 41 } } - }); - }); - - it('tests empty go.mod', async () => { - const deps = await provider.collect(``); - expect(deps).is.eql([]); - }); - - it('tests empty lines in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - - require ( +describe('Golang Gomudules go.mod parser test', () => { + let dependencyProvider: DependencyProvider; - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // Valid data before this. - - github.com/stretchr/testify v1.2.2 - - ) - go 1.13 - - `); - expect(deps.length).equal(2); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 6, column: 41 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.2', position: { line: 8, column: 41 } } - }); - }); - - it('tests deps with spaces before and after comparators', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - require ( - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 - ) - go 1.13 - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 4, column: 44 } } + beforeEach(() => { + dependencyProvider = new DependencyProvider(); }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.1', position: { line: 5, column: 49 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.0.0', position: { line: 6, column: 51 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.2', position: { line: 7, column: 45 } } - }); - }); - - it('tests alpha beta and extra for version in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - - require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible - github.com/davecgh/go-spew v1.1.1+incompatible - github.com/pmezard/go-difflib v1.3.0+version - github.com/stretchr/testify v1.2.2+incompatible-version - github.com/regen-network/protobuf v1.3.2-alpha.regen.4 - github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 - github.com/btcsuite/btcd v0.20.1-beta - ) - go 1.13 - `); - expect(deps.length).equal(8); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.1.3-alpha', position: { line: 5, column: 39 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v2.5.2-alpha+incompatible', position: { line: 6, column: 34 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.1+incompatible', position: { line: 7, column: 38 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.0+version', position: { line: 8, column: 41 } } - }); - expect(deps[4]).is.eql({ - name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.2+incompatible-version', position: { line: 9, column: 39 } } + it('tests empty go.mod', async () => { + const deps = await dependencyProvider.collect(``); + expect(deps).is.eql([]); }); - expect(deps[5]).is.eql({ - name: { value: 'github.com/regen-network/protobuf', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.2-alpha.regen.4', position: { line: 10, column: 45 } } - }); - expect(deps[6]).is.eql({ - name: { value: 'github.com/vmihailenco/msgpack/v5', position: { line: 0, column: 0 } }, - version: { value: 'v5.0.0-beta.1', position: { line: 11, column: 45 } } - }); - expect(deps[7]).is.eql({ - name: { value: 'github.com/btcsuite/btcd', position: { line: 0, column: 0 } }, - version: { value: 'v0.20.1-beta', position: { line: 12, column: 36 } } - }); - }); - it('tests replace statements in go.mod', async () => { - const deps = await provider.collect(` + it('tests require statement in go.mod', async () => { + const deps = await dependencyProvider.collect(` module github.com/alecthomas/kingpin - go 1.13 - require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible - github.com/davecgh/go-spew v1.1.1+incompatible - github.com/pmezard/go-difflib v1.3.0 - github.com/stretchr/testify v1.2.2+incompatible-version - github.com/regen-network/protobuf v1.3.2-alpha.regen.4 - github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 - github.com/btcsuite/btcd v0.0.0-20151022065526-2efee857e7cf - ) - replace ( - github.com/alecthomas/units v0.1.3-alpha => github.com/test-user/units v13.3.2 // Required by OLM - github.com/alecthomas/units v0.1.3 => github.com/test-user/units v13.3.2 // Required by OLM - github.com/pierrec/lz4 => github.com/pierrec/lz4 v3.4.2 // Required by prometheus-operator - github.com/pierrec/lz4 v3.4.1 => github.com/pierrec/lz4 v3.4.3 // same-module with diff version in replace - github.com/davecgh/go-spew v1.1.1+incompatible => github.com/davecgh/go-spew v1.1.2 - github.com/stretchr/testify => github.com/stretchr-1/testify v1.2.3 // test with module and with one import package - github.com/regen-network/protobuf => github.com/regen-network/protobuf1 v1.3.2 // test with module and with one import package - github.com/pmezard/go-difflib v1.3.0 => github.com/pmezard/go-difflib v0.0.0-20151022065526-2efee857e7cf // semver to pseudo - github.com/btcsuite/btcd v0.0.0-20151022065526-2efee857e7cf => github.com/btcsuite/btcd v0.20.1-beta // pseudo to semver - github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 => ./msgpack/v5 // replace with local module - ) - `); - expect(deps.length).equal(8); - expect(deps[0]).is.eql({ - name: { value: 'github.com/test-user/units', position: { line: 0, column: 0 } }, - version: { value: 'v13.3.2', position: { line: 16, column: 82 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v3.4.2', position: { line: 18, column: 60 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.2', position: { line: 20, column: 88 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 23, column: 81 } } - }); - expect(deps[4]).is.eql({ - name: { value: 'github.com/stretchr-1/testify', position: { line: 0, column: 0 } }, - version: { value: 'v1.2.3', position: { line: 21, column: 72 } } - }); - expect(deps[5]).is.eql({ - name: { value: 'github.com/regen-network/protobuf1', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.2', position: { line: 22, column: 83 } } - }); - expect(deps[6]).is.eql({ - name: { value: 'github.com/vmihailenco/msgpack/v5', position: { line: 0, column: 0 } }, - version: { value: 'v5.0.0-beta.1', position: { line: 11, column: 45 } } - }); - expect(deps[7]).is.eql({ - name: { value: 'github.com/btcsuite/btcd', position: { line: 0, column: 0 } }, - version: { value: 'v0.20.1-beta', position: { line: 24, column: 99 } } - }); - }); + require github.com/pmezard/go-difflib v1.0.0 - it('tests single line replace statement in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - go 1.13 require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible + github.com/davecgh/go-spew v1.1.1 ) - replace github.com/alecthomas/units => github.com/test-user/units v13.3.2 - `); - expect(deps.length).equal(2); - expect(deps[0]).is.eql({ - name: { value: 'github.com/test-user/units', position: { line: 0, column: 0 } }, - version: { value: 'v13.3.2', position: { line: 9, column: 75 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v2.5.2-alpha+incompatible', position: { line: 6, column: 34 } } - }); - }); - - it('tests multiple line replace statement in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - go 1.13 - - replace github.com/alecthomas/units => github.com/test-user/units v13.3.2 - - require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible - github.com/davecgh/go-spew v1.1.1+incompatible - github.com/pmezard/go-difflib v1.3.0 - ) - - replace github.com/pierrec/lz4 v2.5.2-alpha+incompatible => github.com/pierrec/lz4 v2.5.3 - replace github.com/davecgh/go-spew => github.com/davecgh/go-spew v1.1.2 - // replace github.com/pmezard/go-difflib v1.3.0 => github.com/pmezard/go-difflib v1.3.1 - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/test-user/units', position: { line: 0, column: 0 } }, - version: { value: 'v13.3.2', position: { line: 5, column: 75 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v2.5.3', position: { line: 14, column: 92 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.2', position: { line: 15, column: 74 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.0', position: { line: 11, column: 41 } } - }); - }); - - - it('tests multiple module points to same replace module in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin go 1.13 - require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible - github.com/gogo/protobuf v1.3.0 - github.com/golang/protobuf v1.3.4 - ) - replace ( - github.com/golang/protobuf => github.com/gogo/protobuf v1.3.1 - github.com/gogo/protobuf v1.3.0 => github.com/gogo/protobuf v1.3.1 - ) - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, - version: { value: 'v0.1.3-alpha', position: { line: 5, column: 39 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v2.5.2-alpha+incompatible', position: { line: 6, column: 34 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/gogo/protobuf', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.1', position: { line: 12, column: 71 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/gogo/protobuf', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.1', position: { line: 11, column: 66 } } - }); - }); - - it('tests replace block before require in go.mod', async () => { - const deps = await provider.collect(` - module github.com/alecthomas/kingpin - go 1.13 - - replace ( - github.com/alecthomas/units => github.com/test-user/units v13.3.2 - github.com/pierrec/lz4 v2.5.2-alpha+incompatible => github.com/pierrec/lz4 v2.5.3 - github.com/davecgh/go-spew => github.com/davecgh/go-spew v1.1.2 - // replace github.com/pmezard/go-difflib v1.3.0 => github.com/pmezard/go-difflib v1.3.1 - ) - - require ( - github.com/alecthomas/units v0.1.3-alpha - github.com/pierrec/lz4 v2.5.2-alpha+incompatible - github.com/davecgh/go-spew v1.1.1+incompatible - github.com/pmezard/go-difflib v1.3.0 - ) - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/test-user/units', position: { line: 0, column: 0 } }, - version: { value: 'v13.3.2', position: { line: 6, column: 69 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, - version: { value: 'v2.5.3', position: { line: 7, column: 86 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, - version: { value: 'v1.1.2', position: { line: 8, column: 68 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.0', position: { line: 16, column: 41 } } + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, + version: { value: 'v1.0.0', position: { line: 4, column: 47 } } + }, + { + name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, + version: { value: 'v1.1.1', position: { line: 7, column: 40 } } + } + ]); + }); + + it('tests go.mod with the same package but different versions', async () => { + const deps = await dependencyProvider.collect(` + module test/data/sample1 + + go 1.15 + + require ( + github.com/googleapis/gax-go v1.0.3 + github.com/googleapis/gax-go/v2 v2.0.5 + ) + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/googleapis/gax-go', position: { line: 0, column: 0 } }, + version: { value: 'v1.0.3', position: { line: 7, column: 46 } } + }, + { + name: { value: 'github.com/googleapis/gax-go/v2', position: { line: 0, column: 0 } }, + version: { value: 'v2.0.5', position: { line: 8, column: 49 } } + } + ]); + }); + + it('tests go.mod with comments', async () => { + const deps = await dependencyProvider.collect(` + // This is the starting point. + module github.com/alecthomas/kingpin + require ( + github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // Valid data before this. + // Extra comment in require section. + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 + ) + go 1.13 + // Final notes. + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 5, column: 45 } } + }, + { + name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, + version: { value: 'v1.0.0', position: { line: 7, column: 47 } } + }, + { + name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, + version: { value: 'v1.2.2', position: { line: 8, column: 45 } } + } + ]); }); - }); - it('tests go.mod with a module and package of different version', async () => { - const deps = await provider.collect(` - module test/data/sample1 + it('tests empty lines in go.mod', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin - go 1.15 + require ( - require ( - github.com/googleapis/gax-go v1.0.3 - github.com/googleapis/gax-go/v2 v2.0.5 - ) - `); - expect(deps.length).equal(2); - expect(deps[0]).is.eql({ - name: { value: 'github.com/googleapis/gax-go', position: { line: 0, column: 0 } }, - version: { value: 'v1.0.3', position: { line: 7, column: 38 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/googleapis/gax-go/v2', position: { line: 0, column: 0 } }, - version: { value: 'v2.0.5', position: { line: 8, column: 41 } } - }); - }); + github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf - it('tests exclude statements in go.mod', async () => { - const deps = await provider.collect(` - module test/data/sample1 + github.com/stretchr/testify v1.2.2 - go 1.15 - require ( - github.com/googleapis/gax-go v1.0.3 - github.com/googleapis/gax-go/v2 v2.0.5 - github.com/gogo/protobuf v1.3.0 - github.com/golang/protobuf v1.3.4 - ) - exclude ( - github.com/googleapis/gax-go v1.0.3 - github.com/googleapis/gax-go/v2 v2.0.5 - ) + ) + go 1.13 - exclude github.com/gogo/protobuf v1.3.0 - - `); - expect(deps.length).equal(4); - expect(deps[0]).is.eql({ - name: { value: 'github.com/googleapis/gax-go', position: { line: 0, column: 0 } }, - version: { value: 'v1.0.3', position: { line: 7, column: 38 } } - }); - expect(deps[1]).is.eql({ - name: { value: 'github.com/googleapis/gax-go/v2', position: { line: 0, column: 0 } }, - version: { value: 'v2.0.5', position: { line: 8, column: 41 } } - }); - expect(deps[2]).is.eql({ - name: { value: 'github.com/gogo/protobuf', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.0', position: { line: 9, column: 34 } } - }); - expect(deps[3]).is.eql({ - name: { value: 'github.com/golang/protobuf', position: { line: 0, column: 0 } }, - version: { value: 'v1.3.4', position: { line: 10, column: 36 } } + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 6, column: 45 } } + }, + { + name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, + version: { value: 'v1.2.2', position: { line: 8, column: 45 } } + } + ]); + }); + + it('tests deps with spaces before and after dep name and version', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin + require ( + github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 // indirect + ) + go 1.13 + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 4, column: 48 } } + }, + { + name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, + version: { value: 'v1.1.1', position: { line: 5, column: 48 } } + }, + { + name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, + version: { value: 'v1.0.0', position: { line: 6, column: 47 } } + }, + { + name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, + version: { value: 'v1.2.2', position: { line: 7, column: 52 } } + } + ]); + }); + + it('tests deps with alpha, beta and extra for version', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin + + require ( + github.com/alecthomas/units v0.1.3-alpha + github.com/pierrec/lz4 v2.5.2-alpha+incompatible + github.com/davecgh/go-spew v1.1.1+incompatible + github.com/pmezard/go-difflib v1.3.0+version + github.com/stretchr/testify v1.2.2+incompatible-version + github.com/regen-network/protobuf v1.3.2-alpha.regen.4 + github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 + github.com/btcsuite/btcd v0.20.1-beta + ) + + go 1.13 + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/alecthomas/units', position: { line: 0, column: 0 } }, + version: { value: 'v0.1.3-alpha', position: { line: 5, column: 45 } } + }, + { + name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, + version: { value: 'v2.5.2-alpha+incompatible', position: { line: 6, column: 40 } } + }, + { + name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, + version: { value: 'v1.1.1+incompatible', position: { line: 7, column: 44 } } + }, + { + name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, + version: { value: 'v1.3.0+version', position: { line: 8, column: 47 } } + }, + { + name: { value: 'github.com/stretchr/testify', position: { line: 0, column: 0 } }, + version: { value: 'v1.2.2+incompatible-version', position: { line: 9, column: 45 } } + }, + { + name: { value: 'github.com/regen-network/protobuf', position: { line: 0, column: 0 } }, + version: { value: 'v1.3.2-alpha.regen.4', position: { line: 10, column: 51 } } + }, + { + name: { value: 'github.com/vmihailenco/msgpack/v5', position: { line: 0, column: 0 } }, + version: { value: 'v5.0.0-beta.1', position: { line: 11, column: 51 } } + }, + { + name: { value: 'github.com/btcsuite/btcd', position: { line: 0, column: 0 } }, + version: { value: 'v0.20.1-beta', position: { line: 12, column: 42 } } + } + ]); + }); + + it('tests replace statements in go.mod', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin + go 1.13 + + replace ( + github.com/alecthomas/units v0.1.3-alpha => github.com/test-user/units v13.3.2 // Required by OLM + github.com/alecthomas/units v0.1.3 => github.com/test-user/units v13.3.2 // Required by OLM + github.com/pierrec/lz4 => github.com/pierrec/lz4 v3.4.2 // Required by prometheus-operator + github.com/pierrec/lz4 v3.4.1 => github.com/pierrec/lz4 v3.4.3 // same-module with diff version in replace + ) + + replace github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 => ./msgpack/v5 // replace with local module + + require ( + github.com/alecthomas/units v0.1.3-alpha + github.com/pierrec/lz4 v2.5.2-alpha+incompatible + github.com/davecgh/go-spew v1.1.1+incompatible + github.com/pmezard/go-difflib v1.3.0 + github.com/stretchr/testify v1.2.2+incompatible-version + github.com/regen-network/protobuf v1.3.2-alpha.regen.4 + github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 + github.com/btcsuite/btcd v0.0.0-20151022065526-2efee857e7cf + ) + + replace ( + github.com/davecgh/go-spew v1.1.1+incompatible => github.com/davecgh/go-spew v1.1.2 + github.com/stretchr/testify => github.com/stretchr-1/testify v1.2.3 // test with module and with one import package + github.com/regen-network/protobuf => github.com/regen-network/protobuf1 v1.3.2 // test with module and with one import package + github.com/pmezard/go-difflib v1.3.0 => github.com/pmezard/go-difflib v0.0.0-20151022065526-2efee857e7cf // semver to pseudo + ) + + replace github.com/btcsuite/btcd v0.0.0-20151022065526-2efee857e7cf => github.com/btcsuite/btcd v0.20.1-beta // pseudo to semver + `); + expect(deps).is.eql([ + { + name: { value: 'github.com/test-user/units', position: { line: 0, column: 0 } }, + version: { value: 'v13.3.2', position: { line: 6, column: 88 } } + }, + { + name: { value: 'github.com/pierrec/lz4', position: { line: 0, column: 0 } }, + version: { value: 'v3.4.2', position: { line: 8, column: 66 } } + }, + { + name: { value: 'github.com/davecgh/go-spew', position: { line: 0, column: 0 } }, + version: { value: 'v1.1.2', position: { line: 26, column: 94 } } + }, + { + name: { value: 'github.com/pmezard/go-difflib', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20151022065526-2efee857e7cf', position: { line: 29, column: 87 } } + }, + { + name: { value: 'github.com/stretchr-1/testify', position: { line: 0, column: 0 } }, + version: { value: 'v1.2.3', position: { line: 27, column: 78 } } + }, + { + name: { value: 'github.com/regen-network/protobuf1', position: { line: 0, column: 0 } }, + version: { value: 'v1.3.2', position: { line: 28, column: 89 } } + }, + { + name: { value: 'github.com/vmihailenco/msgpack/v5', position: { line: 0, column: 0 } }, + version: { value: 'v5.0.0-beta.1', position: { line: 21, column: 51 } } + }, + { + name: { value: 'github.com/btcsuite/btcd', position: { line: 0, column: 0 } }, + version: { value: 'v0.20.1-beta', position: { line: 32, column: 109 } } + } + ]); + }); + + it('tests multiple module points to same replace module in go.mod', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin + go 1.13 + + require ( + cloud.google.com/go v0.100.2 + github.com/gogo/protobuf v1.3.0 + github.com/golang/protobuf v1.3.4 + ) + + replace ( + github.com/golang/protobuf => github.com/gogo/protobuf v1.3.1 + github.com/gogo/protobuf v1.3.0 => github.com/gogo/protobuf v1.3.1 + ) + `); + expect(deps).is.eql([ + { + name: { value: 'cloud.google.com/go', position: { line: 0, column: 0 } }, + version: { value: 'v0.100.2', position: { line: 6, column: 37 } } + }, + { + name: { value: 'github.com/gogo/protobuf', position: { line: 0, column: 0 } }, + version: { value: 'v1.3.1', position: { line: 13, column: 77 } } + }, + { + name: { value: 'github.com/gogo/protobuf', position: { line: 0, column: 0 } }, + version: { value: 'v1.3.1', position: { line: 12, column: 72 } } + } + ]); + }); + + it('tests exclude statements in go.mod', async () => { + const deps = await dependencyProvider.collect(` + module github.com/alecthomas/kingpin + go 1.13 + + exclude ( + golang.org/x/crypto v1.4.5 + ) + + exclude github.com/gogo/protobuf + + require ( + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + cloud.google.com/go/compute v1.6.1 + ) + + exclude ( + cloud.google.com/go v0.100.2 + ) + + exclude golang.org/x/sys v1.6.7 + `); + expect(deps).is.eql([ + { + name: { value: 'golang.org/x/sys', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20220728004956-3c1f35247d10', position: { line: 12, column: 34 } } + }, + { + name: { value: 'golang.org/x/sync', position: { line: 0, column: 0 } }, + version: { value: 'v0.0.0-20210220032951-036812b2e83c', position: { line: 13, column: 35 } } + }, + { + name: { value: 'cloud.google.com/go/compute', position: { line: 0, column: 0 } }, + version: { value: 'v1.6.1', position: { line: 14, column: 45 } } + } + ]); }); - }); }); + diff --git a/test/providers/package.json.test.ts b/test/providers/package.json.test.ts index 8e2e3b26..482e4595 100644 --- a/test/providers/package.json.test.ts +++ b/test/providers/package.json.test.ts @@ -1,135 +1,179 @@ +'use strict'; + import { expect } from 'chai'; +import * as sinon from 'sinon'; import { DependencyProvider } from '../../src/providers/package.json'; -describe('NPM package.json parser test', () => { - const collector = new DependencyProvider(); +describe('Javascript NPM package.json parser test', () => { + let dependencyProvider: DependencyProvider; + + beforeEach(() => { + dependencyProvider = new DependencyProvider(); + }); it('tests empty package.json file content', async () => { - const deps = await collector.collect(``); + const deps = await dependencyProvider.collect(``); expect(deps.length).equal(0); }); it('tests empty package.json', async () => { - const deps = await collector.collect(`{}`); + const deps = await dependencyProvider.collect(`{}`); expect(deps.length).equal(0); }); it('tests invalid token package.json', async () => { - const deps = await collector.collect(` - { - <<<<< - "dependencies": { - "hello": "1.0", - } - } + const deps = await dependencyProvider.collect(` + { + <<<<< + "dependencies": { + "hello": "1.0", + } + } `); expect(deps).eql([]); }); it('tests invalid package.json', async () => { - const deps = await collector.collect(` - { - "dependencies": { - "hello": "1.0", - } - } + const deps = await dependencyProvider.collect(` + { + "dependencies": { + "hello": "1.0", + } + } `); expect(deps).eql([]); }); it('tests empty dependencies key', async () => { - const deps = await collector.collect(` - { - "hello":[], - "dependencies": {} - } + const deps = await dependencyProvider.collect(` + { + "hello":[], + "dependencies": {} + } `); - expect(deps.length).equal(0); + expect(deps).eql([]); }); it('tests single dependency ', async () => { - const deps = await collector.collect(`{ - "hello":{}, - "dependencies": { - "hello": "1.0" - } - }`); - expect(deps).is.eql([{ - name: {value: "hello", position: {line: 4, column: 14}}, - version: {value: "1.0", position: {line: 4, column: 23}} - }]); + const deps = await dependencyProvider.collect(` + { + "hello":{}, + "dependencies": { + "hello": "1.0" + } + } + `); + expect(deps).is.eql([ + { + name: {value: "hello", position: {line: 5, column: 22}}, + version: {value: "1.0", position: {line: 5, column: 31}} + } + ]); }); - it('tests single dependency as devDependencies', async () => { - let collector = new DependencyProvider(["devDependencies"]); - let deps = await collector.collect(`{ - "devDependencies": { - "hello": "1.0" - }, - "dependencies": { - "foo": "10.1.1" - } - }`); - expect(deps).is.eql([{ - name: {value: "hello", position: {line: 3, column: 14}}, - version: {value: "1.0", position: {line: 3, column: 23}} - }]); - - collector = new DependencyProvider(["devDependencies", "dependencies"]); - deps = await collector.collect(`{ - "devDependencies": { - "hello": "1.0" - }, - "dependencies": { - "foo": "10.1.1" - } - }`); - expect(deps).is.eql([{ - name: {value: "hello", position: {line: 3, column: 14}}, - version: {value: "1.0", position: {line: 3, column: 23}} - },{ - name: {value: "foo", position: {line: 6, column: 14}}, - version: {value: "10.1.1", position: {line: 6, column: 21}} - }]); + it('tests dependency in devDependencies class', async () => { + dependencyProvider.classes = ["devDependencies"]; + let deps = await dependencyProvider.collect(` + { + "devDependencies": { + "hello": "1.0" + }, + "dependencies": { + "foo": "10.1.1" + } + } + `); + expect(deps).is.eql([ + { + name: {value: "hello", position: {line: 4, column: 22}}, + version: {value: "1.0", position: {line: 4, column: 31}} + } + ]); }); + it('tests dependency in multiple classes', async () => { + dependencyProvider.classes = ["devDependencies", "dependencies"]; + let deps = await dependencyProvider.collect(` + { + "devDependencies": { + "hello": "1.0" + }, + "dependencies": { + "foo": "10.1.1" + } + } + `); + expect(deps).is.eql([ + { + name: {value: "hello", position: {line: 4, column: 22}}, + version: {value: "1.0", position: {line: 4, column: 31}} + }, + { + name: {value: "foo", position: {line: 7, column: 22}}, + version: {value: "10.1.1", position: {line: 7, column: 29}} + } + ]); + }); - it('tests single dependency with version in next line', async () => { - const deps = await collector.collect(`{ - "hello":{}, - "dependencies": { - "hello": - "1.0" - } - }`); - expect(deps).is.eql([{ - name: {value: "hello", position: {line: 4, column: 14}}, - version: {value: "1.0", position: {line: 5, column: 16}} - }]); + + it('tests dependency with version in next line', async () => { + const deps = await dependencyProvider.collect(` + { + "hello":{}, + "dependencies": { + "hello": + "1.0" + } + } + `); + expect(deps).is.eql([ + { + name: {value: "hello", position: {line: 5, column: 22}}, + version: {value: "1.0", position: {line: 6, column: 22}} + } + ]); }); it('tests 3 dependencies with spaces', async () => { - const deps = await collector.collect(`{ - "hello":{}, - "dependencies": { - "hello": "1.0", - "world":"^1.0", - - - "foo": - - " 10.0.1" - } - }`); - expect(deps).is.eql([{ - name: {value: "hello", position: {line: 4, column: 13}}, - version: {value: "1.0", position: {line: 4, column: 37}} - },{ - name: {value: "world", position: {line: 5, column: 16}}, - version: {value: "^1.0", position: {line: 5, column: 24}} - },{ - name: {value: "foo", position: {line: 8, column: 10}}, - version: {value: " 10.0.1", position: {line: 10, column: 12}} - }]); + const deps = await dependencyProvider.collect(` + { + "hello":{}, + "dependencies": { + "hello": "1.0", + "world":"^1.0", + + + "foo": + + " 10.0.1" + } + } + `); + expect(deps).is.eql([ + { + name: {value: "hello", position: {line: 5, column: 18}}, + version: {value: "1.0", position: {line: 5, column: 42}} + }, + { + name: {value: "world", position: {line: 6, column: 22}}, + version: {value: "^1.0", position: {line: 6, column: 30}} + }, + { + name: {value: "foo", position: {line: 9, column: 18}}, + version: {value: " 10.0.1", position: {line: 11, column: 18}} + } + ]); + }); + + it('should throw an error for invalid JSON content', async () => { + sinon.stub(dependencyProvider, 'parseJson').throws(new Error('Mock error message')); + + try { + await dependencyProvider.collect(``); + throw new Error('Expected an error to be thrown'); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('Mock error message'); + } }); }); diff --git a/test/providers/pom.xml.test.ts b/test/providers/pom.xml.test.ts index 24269919..f6ca02ca 100644 --- a/test/providers/pom.xml.test.ts +++ b/test/providers/pom.xml.test.ts @@ -3,242 +3,262 @@ import { expect } from 'chai'; import { DependencyProvider } from '../../src/providers/pom.xml'; -describe('Maven pom.xml parser test', () => { +describe('Java Maven pom.xml parser test', () => { + let dependencyProvider: DependencyProvider; + + beforeEach(() => { + dependencyProvider = new DependencyProvider(); + }); it('tests pom.xml with empty string', async () => { - const pom = ` - `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const pom = ``; + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); it('tests pom.xml with empty project', async () => { - const pom = ` + const pom = ` + - + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); - it('tests pom.xml with empty project + dependencies', async () => { - const pom = ` - + it('tests pom.xml with empty dependencies', async () => { + const pom = ` + + - - + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); it('tests valid pom.xml', async () => { const pom = ` - - - - c - ab-cd - 2.3 - test - - - foo - bar - 2.4 - - - + + + + c + ab-cd + 2.3 + test + + + foo + bar + 2.4 + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'foo/bar', position: { line: 10, column: 17 } }, - version: { value: '2.4', position: { line: 13, column: 30 } }, - }]); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'foo/bar', position: { line: 10, column: 21 } }, + version: { value: '2.4', position: { line: 13, column: 34 } }, + } + ]); }); - it('highlights duplicate dependencies', async () => { + it('tests duplicate dependencies', async () => { const pom = ` - - - - c - ab-cd - 2.3 - test - - - foo - bar - 2.4 - - - foo - bar - 2.4 - - - + + + + c + ab-cd + 2.3 + test + + + foo + bar + 2.4 + + + foo + bar + 2.4 + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'foo/bar', position: { line: 10, column: 17 } }, - version: { value: '2.4', position: { line: 13, column: 30 } }, - },{ - name: { value: 'foo/bar', position: { line: 15, column: 17 } }, - version: { value: '2.4', position: { line: 18, column: 30 } }, - }]); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'foo/bar', position: { line: 10, column: 21 } }, + version: { value: '2.4', position: { line: 13, column: 34 } }, + }, + { + name: { value: 'foo/bar', position: { line: 15, column: 21 } }, + version: { value: '2.4', position: { line: 18, column: 34 } }, + } + ]); }); - it('highlights duplicate dependencies when one has version', async () => { + it('tests duplicate dependencies when only one does not specify a version', async () => { const pom = ` - - - - c - ab-cd - 2.3 - test - - - foo - bar - 2.4 - - - foo - bar - - - + + + + c + ab-cd + 2.3 + test + + + foo + bar + 2.4 + + + foo + bar + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'foo/bar', position: { line: 10, column: 17 } }, - version: { value: '2.4', position: { line: 13, column: 30 } }, - },{ - name: { value: 'foo/bar', position: { line: 15, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: ` - foo - bar - __VERSION__ - `, - range: { - end: { - character: 29, - line: 17 - }, - start: { - character: 16, - line: 14 + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'foo/bar', position: { line: 10, column: 21 } }, + version: { value: '2.4', position: { line: 13, column: 34 } }, + }, + { + name: { value: 'foo/bar', position: { line: 15, column: 21 } }, + context: { value: + ` + foo + bar + __VERSION__ + `, + range: { + end: { + character: 33, + line: 17 + }, + start: { + character: 20, + line: 14 + } } } } - }]); + ]); }); - it('highlights duplicate dependencies when none has version', async () => { + it('test duplicate dependencies where none specify a version', async () => { const pom = ` - - - - c - ab-cd - 2.3 - test - - - foo - bar - - - foo - bar - - - + + + + c + ab-cd + 2.3 + test + + + foo + bar + + + foo + bar + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'foo/bar', position: { line: 10, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: ` - foo - bar - __VERSION__ - `, - range: { - end: { - character: 29, - line: 12 - }, - start: { - character: 16, - line: 9 + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'foo/bar', position: { line: 10, column: 21 } }, + context: { value: + ` + foo + bar + __VERSION__ + `, + range: { + end: { + character: 33, + line: 12 + }, + start: { + character: 20, + line: 9 + } } } - } - },{ - name: { value: 'foo/bar', position: { line: 14, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: - ` - foo - bar - __VERSION__ - `, - range: { - end: { - character: 29, - line: 16 - }, - start: { - character: 16, - line: 13 + }, + { + name: { value: 'foo/bar', position: { line: 14, column: 21 } }, + context: { value: + ` + foo + bar + __VERSION__ + `, + range: { + end: { + character: 33, + line: 16 + }, + start: { + character: 20, + line: 13 + } } } } - }]); + ]); }); it('tests pom.xml with multiple dependencies', async () => { const pom = ` - - + + + + + plugins + a + 2.3 + + + - plugins + dep a - 2.3 + 10.1 + + + foo + bar + 2.4 - - - - dep - a - 10.1 - - - foo - bar - 2.4 - - - + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'plugins/a', position: { line: 5, column: 21 } }, - version: { value: '2.3', position: { line: 8, column: 34 } } - }, { - name: { value: 'dep/a', position: { line: 13, column: 17 } }, - version: { value: '10.1', position: { line: 16, column: 30 } } - }, { - name: { value: 'foo/bar', position: { line: 18, column: 17 } }, - version: { value: '2.4', position: { line: 21, column: 30 } } - }]); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'plugins/a', position: { line: 5, column: 25 } }, + version: { value: '2.3', position: { line: 8, column: 38 } } + }, + { + name: { value: 'dep/a', position: { line: 13, column: 21 } }, + version: { value: '10.1', position: { line: 16, column: 34 } } + }, + { + name: { value: 'foo/bar', position: { line: 18, column: 21 } }, + version: { value: '2.4', position: { line: 21, column: 34 } } + } + ]); }); it('tests pom.xml with only test scope', async () => { @@ -270,228 +290,234 @@ describe('Maven pom.xml parser test', () => { `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); it('tests pom.xml with invalid dependencies', async () => { const pom = ` - - - - - ab-cd - - - c - - - - c - - - - - - + + + + + ab-cd + + + c + + + + c + + + + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); it('tests pom.xml with invalid dependency versions', async () => { const pom = ` - - - - c - ab-cd - - - - c - ab-cd - - - - + + + + c + ab-cd + + + + c + ab-cd + + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'c/ab-cd', position: { line: 4, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: - ` - c - ab-cd - __VERSION__ - `, - range: { - end: { - character: 29, - line: 7 - }, - start: { - character: 16, - line: 3 + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'c/ab-cd', position: { line: 4, column: 21 } }, + context: { value: + ` + c + ab-cd + __VERSION__ + `, + range: { + end: { + character: 33, + line: 7 + }, + start: { + character: 20, + line: 3 + } } } - } - },{ - name: { value: 'c/ab-cd', position: { line: 9, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: - ` - c - ab-cd - __VERSION__ - `, - range: { - end: { - character: 30, - line: 11 - }, - start: { - character: 16, - line: 8 + }, + { + name: { value: 'c/ab-cd', position: { line: 9, column: 21 } }, + context: { value: + ` + c + ab-cd + __VERSION__ + `, + range: { + end: { + character: 34, + line: 11 + }, + start: { + character: 20, + line: 8 + } } } } - }]); + ]); }); it('tests pom.xml with dependencyManagement scope', async () => { const pom = ` - - - - - {a.groupId} - bc - {a.version} - runtime - - - - a - b-c - 1.2.3 - compile - true - - - - - c - ab-cd - 2.3 - - - foo - bar - 2.4 - - - + + + + + {a.groupId} + bc + {a.version} + runtime + + + + a + b-c + 1.2.3 + compile + true + + + + + c + ab-cd + 2.3 + + + foo + bar + 2.4 + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'c/ab-cd', position: { line: 21, column: 17 } }, - version: { value: '2.3', position: { line: 24, column: 30 } } - }, { - name: { value: 'foo/bar', position: { line: 26, column: 17 } }, - version: { value: '2.4', position: { line: 29, column: 30 } } - }]); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'c/ab-cd', position: { line: 21, column: 21 } }, + version: { value: '2.3', position: { line: 24, column: 34 } } + }, + { + name: { value: 'foo/bar', position: { line: 26, column: 21 } }, + version: { value: '2.4', position: { line: 29, column: 34 } } + } + ]); }); it('tests pom.xml without version and with properties', async () => { const pom = ` - - - - c - ab-cd - 2.3 - - - \${some.example} - \${other.example} - - - c - ab-other - - - + + + + c + ab-cd + 2.3 + + + \${some.example} + \${other.example} + + + c + ab-other + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps).is.eql([{ - name: { value: 'c/ab-cd', position: { line: 4, column: 17 } }, - version: { value: '2.3', position: { line: 7, column: 30 } } - }, { - name: { value: '${some.example}/${other.example}', position: { line: 9, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: ` - \${some.example} - \${other.example} - __VERSION__ - `, - range: { - end: { - character: 29, - line: 11 - }, - start: { - character: 16, - line: 8 + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([ + { + name: { value: 'c/ab-cd', position: { line: 4, column: 21 } }, + version: { value: '2.3', position: { line: 7, column: 34 } } + }, + { + name: { value: '${some.example}/${other.example}', position: { line: 9, column: 21 } }, + context: { value: ` + \${some.example} + \${other.example} + __VERSION__ + `, + range: { + end: { + character: 33, + line: 11 + }, + start: { + character: 20, + line: 8 + } } } - } - }, { - name: { value: 'c/ab-other', position: { line: 13, column: 17 } }, - version: { value: '', position: { line: 0, column: 0 } }, - context: { value: ` - c - ab-other - __VERSION__ - `, - range: { - end: { - character: 29, - line: 15 - }, - start: { - character: 16, - line: 12 + }, + { + name: { value: 'c/ab-other', position: { line: 13, column: 21 } }, + context: { value: ` + c + ab-other + __VERSION__ + `, + range: { + end: { + character: 33, + line: 15 + }, + start: { + character: 20, + line: 12 + } } } } - }]); + ]); }); it('tests pom.xml with only dependencyManagement scope', async () => { const pom = ` - - - - - {a.groupId} - bc - {a.version} - runtime - - - - a - b-c - 1.2.3 - compile - true - - - + + + + + {a.groupId} + bc + {a.version} + runtime + + + + a + b-c + 1.2.3 + compile + true + + + `; - const deps = await new DependencyProvider().collect(pom); - expect(deps.length).equal(0); + const deps = await dependencyProvider.collect(pom); + expect(deps).is.eql([]); }); }); diff --git a/test/providers/requirements.txt.test.ts b/test/providers/requirements.txt.test.ts index 11cc4555..64078f34 100644 --- a/test/providers/requirements.txt.test.ts +++ b/test/providers/requirements.txt.test.ts @@ -1,80 +1,98 @@ +'use strict'; + import { expect } from 'chai'; import { DependencyProvider } from '../../src/providers/requirements.txt'; -describe('PyPi requirements.txt parser test', () => { - const collector = new DependencyProvider(); +describe('Python PyPi requirements.txt parser test', () => { + let dependencyProvider: DependencyProvider; + + beforeEach(() => { + dependencyProvider = new DependencyProvider(); + }); + + it('tests empty requirements.txt', async () => { + const deps = await dependencyProvider.collect(``); + expect(deps).is.eql([]); + }); it('tests valid requirements.txt', async () => { - const deps = await collector.collect(`a==1 + const deps = await dependencyProvider.collect(` + a==1 B==2.1.1 c>=10.1 d<=20.1.2.3.4.5.6.7.8 `); - expect(deps).is.eql([{ - name: {value: 'a', position: {line: 0, column: 0}}, - version: {value: '1', position: {line: 1, column: 4}} - },{ - name: {value: 'b', position: {line: 0, column: 0}}, - version: {value: '2.1.1', position: {line: 2, column: 16}} - },{ - name: {value: 'c', position: {line: 0, column: 0}}, - version: {value: '10.1', position: {line: 3, column: 16}} - },{ - name: {value: 'd', position: {line: 0, column: 0}}, - version: {value: '20.1.2.3.4.5.6.7.8', position: {line: 4, column: 16}} - }]); + expect(deps).is.eql([ + { + name: {value: 'a', position: {line: 0, column: 0}}, + version: {value: '1', position: {line: 2, column: 16}} + }, + { + name: {value: 'b', position: {line: 0, column: 0}}, + version: {value: '2.1.1', position: {line: 3, column: 16}} + }, + { + name: {value: 'c', position: {line: 0, column: 0}}, + version: {value: '10.1', position: {line: 4, column: 16}} + }, + { + name: {value: 'd', position: {line: 0, column: 0}}, + version: {value: '20.1.2.3.4.5.6.7.8', position: {line: 5, column: 16}} + } + ]); }); it('tests requirements.txt with comments', async () => { - const deps = await collector.collect(`# hello world + const deps = await dependencyProvider.collect(` + # hello world a==1 # hello - # another comment b==2.1.1 - c # yet another comment >=10.1 + # invalid line b==2.1.1 + c # invalid line >=10.1 d<=20.1.2.3.4.5.6.7.8 # done `); - expect(deps).is.eql([{ - name: {value: 'a', position: {line: 0, column: 0}}, - version: {value: '1', position: {line: 2, column: 16}} - },{ - name: {value: 'c', position: {line: 0, column: 0}}, - version: {value: '', position: {line: 4, column: 1}} // column shouldn't matter for empty versions - },{ - name: {value: 'd', position: {line: 0, column: 0}}, - version: {value: '20.1.2.3.4.5.6.7.8', position: {line: 5, column: 16}} - }]); - }); - - it('tests empty requirements.txt', async () => { - const deps = await collector.collect(``); - expect(deps).is.eql([]); + expect(deps).is.eql([ + { + name: {value: 'a', position: {line: 0, column: 0}}, + version: {value: '1', position: {line: 3, column: 16}} + }, + { + name: {value: 'd', position: {line: 0, column: 0}}, + version: {value: '20.1.2.3.4.5.6.7.8', position: {line: 6, column: 16}} + } + ]); }); it('tests empty lines', async () => { - const deps = await collector.collect(` + const deps = await dependencyProvider.collect(` a==1 `); - expect(deps).is.eql([{ - name: {value: 'a', position: {line: 0, column: 0}}, - version: {value: '1', position: {line: 3, column: 16}} - }]); + expect(deps).is.eql([ + { + name: {value: 'a', position: {line: 0, column: 0}}, + version: {value: '1', position: {line: 3, column: 16}} + } + ]); }); it('tests deps with spaces before and after comparators', async () => { - const deps = await collector.collect(` + const deps = await dependencyProvider.collect(` a ==1 - b <= 10.1 + b <= 10.1 `); - expect(deps).is.eql([{ - name: {value: 'a', position: {line: 0, column: 0}}, - version: {value: '1', position: {line: 2, column: 24}} - },{ - name: {value: 'b', position: {line: 0, column: 0}}, - version: {value: '10.1', position: {line: 4, column: 35}} - }]); + expect(deps).is.eql([ + { + name: {value: 'a', position: {line: 0, column: 0}}, + version: {value: '1', position: {line: 2, column: 24}} + }, + { + name: {value: 'b', position: {line: 0, column: 0}}, + version: {value: '10.1', position: {line: 4, column: 33}} + } + ]); }); }); diff --git a/test/vulnerability.test.ts b/test/vulnerability.test.ts index 1e0581b6..9fdfd4a9 100644 --- a/test/vulnerability.test.ts +++ b/test/vulnerability.test.ts @@ -1,131 +1,107 @@ +'use strict'; + import { expect } from 'chai'; -import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver'; -import { DependencyProvider as PomXml } from '../src/providers/pom.xml'; -import { Vulnerability, ANALYTICS_SOURCE } from '../src/vulnerability'; +import { Range } from 'vscode-languageserver'; +import { DependencyProvider } from '../src/providers/pom.xml'; +import { Vulnerability } from '../src/vulnerability'; describe('Vulnerability tests', () => { - const dummyRange: Range = { + const mavenDependencyProvider: DependencyProvider = new DependencyProvider(); + const mockMavenRef: string = 'pkg:maven/mockGroupId1/mockArtifact1@mockVersion'; + const mockRange: Range = { start: { - line: 3, - character: 4 + line: 123, + character: 123 }, end: { - line: 3, - character: 10 + line: 456, + character: 456 } }; - // it('Test vulnerability with minimal fields/without vulnerabilities and without recommendations', async () => { - // let vulnerability = new Vulnerability( - // dummyRange - // ); - - // const msg = '\nRecommendation: No RedHat packages to recommend'; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Information, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - // expect(vulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - // }); - - // it('Test vulnerability without vulnerabilities and with recommendations', async () => { - // let vulnerability = new Vulnerability( - // dummyRange, - // 0, - // 'MockRef', - // {name: 'mockRecommendationName', version: 'mockRecommendationVersion'}, - // 'mockRecommendationName', - // 'mockRecommendationVersion', - // null, - // '' - // ); - - // const msg = 'MockRef\nRecommendation: mockRecommendationName:mockRecommendationVersion'; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Information, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - // expect(vulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - // }); - - // it('Test vulnerability with vulnerabilities and without remediations', async () => { - // let vulnerability = new Vulnerability( - // dummyRange, - // 1, - // 'MockRef', - // null, - // '', - // '', - // null, - // 'MockSeverity' - // ); - - // const msg = "MockRef\nKnown security vulnerabilities: 1\nHighest severity: MockSeverity\nHas remediation: No"; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Error, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - // expect(vulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - // }); - - // it('Test vulnerability with vulnerabilities and with remediations', async () => { - // let vulnerability = new Vulnerability( - // dummyRange, - // 1, - // 'MockRef', - // null, - // '', - // '', - // {mockCVE: 'mockCVE'}, - // 'MockSeverity' - // ); - - // const msg = "MockRef\nKnown security vulnerabilities: 1\nHighest severity: MockSeverity\nHas remediation: Yes"; - // let expectedDiagnostic: Diagnostic = { - // severity: DiagnosticSeverity.Error, - // range: dummyRange, - // message: msg, - // source: ANALYTICS_SOURCE, - // }; - - // expect(vulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); - // }); - - it('Test vulnerability with minimal fields and without vulnerabilities', async () => { + class MockDependencyData { + constructor( + public sourceId: string, + public issuesCount: number, + public highestVulnerabilitySeverity: string + ) {} + } + + it('should return diagnostic without vulnerabilities from single source', async () => { + const mockDependencyData: MockDependencyData[] = [ + new MockDependencyData('snyk-snyk', 0, 'NONE') + ]; let vulnerability = new Vulnerability( - dummyRange, - 'pkg:maven/MockPkg@1.2.3' - ); - - expect(vulnerability.getDiagnostic()).to.eql(undefined); + mavenDependencyProvider, + mockRange, + mockMavenRef, + mockDependencyData + ); + + const diagnostic = vulnerability.getDiagnostic(); + expect(diagnostic.message.replace(/\s/g, "")).to.eql(` + mockGroupId1/mockArtifact1@mockVersion + + snyk-snyk vulnerability info: + Known security vulnerabilities: 0 + Highest severity: NONE + `.replace(/\s/g, "")); }); - it('Test vulnerability with vulnerabilities', async () => { + it('should return diagnostic with vulnerabilities from single source', async () => { + const mockDependencyData: MockDependencyData[] = [ + new MockDependencyData('snyk-snyk', 2, 'HIGH') + ]; let vulnerability = new Vulnerability( - dummyRange, - 'pkg:maven/MockPkg@1.2.3', - 1, - 'MockSeverity' - ); - vulnerability.provider = new PomXml(); - - const msg = "MockRef\nKnown security vulnerabilities: 1\nHighest severity: MockSeverity"; - let expectedDiagnostic: Diagnostic = { - severity: DiagnosticSeverity.Error, - range: dummyRange, - message: msg, - source: ANALYTICS_SOURCE, - }; + mavenDependencyProvider, + mockRange, + mockMavenRef, + mockDependencyData + ); + + const diagnostic = vulnerability.getDiagnostic(); + expect(diagnostic.message.replace(/\s/g, "")).to.eql(` + mockGroupId1/mockArtifact1@mockVersion + + snyk-snyk vulnerability info: + Known security vulnerabilities: 2 + Highest severity: HIGH + `.replace(/\s/g, "")); + }); - expect(vulnerability.getDiagnostic().toString().replace(/\s/g, "")).is.eql(expectedDiagnostic.toString().replace(/\s/g, "")); + it('should return diagnostic from multiple source', async () => { + const mockDependencyData: MockDependencyData[] = [ + new MockDependencyData('snyk-snyk', 1, 'HIGH'), + new MockDependencyData('snyk-oss', 2, 'LOW'), + new MockDependencyData('oss-oss', 0, 'NONE'), + new MockDependencyData('oss-snyk', 5, 'HIGH') + ]; + let vulnerability = new Vulnerability( + mavenDependencyProvider, + mockRange, + mockMavenRef, + mockDependencyData + ); + + const diagnostic = vulnerability.getDiagnostic(); + expect(diagnostic.message.replace(/\s/g, "")).to.eql(` + mockGroupId1/mockArtifact1@mockVersion + + snyk-snyk vulnerability info: + Known security vulnerabilities: 1 + Highest severity: HIGH + + snyk-oss vulnerability info: + Known security vulnerabilities: 2 + Highest severity: LOW + + oss-oss vulnerability info: + Known security vulnerabilities: 0 + Highest severity: NONE + + oss-snyk vulnerability info: + Known security vulnerabilities: 5 + Highest severity: HIGH + `.replace(/\s/g, "")); }); }); From aed8c8c441477b22e9e309cabd1c4d6e110dd669 Mon Sep 17 00:00:00 2001 From: Ilona Shishov Date: Wed, 22 Nov 2023 11:33:07 +0200 Subject: [PATCH 6/6] feat: add OSS Index dependency provider Signed-off-by: Ilona Shishov --- .github/workflows/release.yml | 4 ++-- package-lock.json | 8 ++++---- package.json | 2 +- src/collector.ts | 4 ++-- src/componentAnalysis.ts | 4 ++++ src/config.ts | 6 ++++++ src/diagnosticsHandler.ts | 2 +- src/server.ts | 1 - 8 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fdb066f..5b380151 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: - name: Update package with new version id: bump run: | - echo "version=$(npm version prerelease --no-git-tag-version --preid ea)" >> "$GITHUB_OUTPUT" + echo "version=$(npm version ${{ vars.VERSION_BUMPING_TYPE }} --no-git-tag-version --preid ea)" >> "$GITHUB_OUTPUT" - name: Install project modules run: npm ci @@ -75,7 +75,7 @@ jobs: const response = await github.request('POST /repos/' + repo_name + '/releases', { tag_name: '${{ steps.bump.outputs.version }}', name: '${{ steps.bump.outputs.version }}', - prerelease: true, + prerelease: false, generate_release_notes: true }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6130f886..ebb6dbf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.7.1-ea.19", "license": "Apache-2.0", "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.0", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.0", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", @@ -837,9 +837,9 @@ } }, "node_modules/@RHEcosystemAppEng/exhort-javascript-api": { - "version": "0.1.0", - "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.0/3159c652ef143fbd27ea8decb881d0f460384cdf", - "integrity": "sha512-YstZq1eAUYitnu5DKP3jcfug93B0EAmsK88FAgLQFp5fmFSaCnpnbkyeKpLzulGC4T5bgnq/t1/QswkAWtdoLg==", + "version": "0.1.1-ea.0", + "resolved": "https://npm.pkg.github.com/download/@RHEcosystemAppEng/exhort-javascript-api/0.1.1-ea.0/4a88e1a897e5698b84b44d5abedd9721b557385e", + "integrity": "sha512-g1UOcBKZDQWTjb2ad+nyealJj2jryVCh6DpQoq+j03tWjlUAbR7OsEVunYDjlASXeVH9UOHENqAnCod+xUCSwA==", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.23.2", diff --git a/package.json b/package.json index ed181c10..95fea37c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dist" ], "dependencies": { - "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.0", + "@RHEcosystemAppEng/exhort-javascript-api": "^0.1.1-ea.0", "@xml-tools/ast": "^5.0.5", "@xml-tools/parser": "^1.0.11", "json-to-ast": "^2.1.0", diff --git a/src/collector.ts b/src/collector.ts index b1121ef0..b32aee60 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -44,8 +44,8 @@ export interface IDependency { * Represents a dependency and implements the IDependency interface. */ export class Dependency implements IDependency { - public version: IPositionedString - public context: IPositionedContext + public version: IPositionedString; + public context: IPositionedContext; constructor( public name: IPositionedString diff --git a/src/componentAnalysis.ts b/src/componentAnalysis.ts index b81ae5f0..ffc20dff 100644 --- a/src/componentAnalysis.ts +++ b/src/componentAnalysis.ts @@ -123,6 +123,10 @@ async function componentAnalysisService (fileType: string, contents: string): Pr if (globalConfig.exhortSnykToken !== '') { options['EXHORT_SNYK_TOKEN'] = globalConfig.exhortSnykToken; } + if (globalConfig.exhortOSSIndexUser !== '' && globalConfig.exhortOSSIndexToken !== '') { + options['EXHORT_OSS_INDEX_USER'] = globalConfig.exhortOSSIndexUser; + options['EXHORT_OSS_INDEX_TOKEN'] = globalConfig.exhortOSSIndexToken; + } const componentAnalysisJson = await exhort.componentAnalysis(fileType, contents, options); // Execute component analysis diff --git a/src/config.ts b/src/config.ts index 9617678a..108c2780 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,8 @@ export class Config utmSource: string; exhortDevMode: string; exhortSnykToken: string; + exhortOSSIndexUser: string; + exhortOSSIndexToken: string; matchManifestVersions: string; exhortMvnPath: string; exhortNpmPath: string; @@ -33,6 +35,8 @@ export class Config this.utmSource = process.env.VSCEXT_UTM_SOURCE || ''; this.exhortDevMode = process.env.VSCEXT_EXHORT_DEV_MODE || 'false'; this.exhortSnykToken = process.env.VSCEXT_EXHORT_SNYK_TOKEN || ''; + this.exhortOSSIndexUser = process.env.VSCEXT_EXHORT_OSS_INDEX_USER || ''; + this.exhortOSSIndexToken = process.env.VSCEXT_EXHORT_OSS_INDEX_TOKEN || ''; this.matchManifestVersions = process.env.VSCEXT_MATCH_MANIFEST_VERSIONS || 'true'; this.exhortMvnPath = process.env.VSCEXT_EXHORT_MVN_PATH || 'mvn'; this.exhortNpmPath = process.env.VSCEXT_EXHORT_NPM_PATH || 'npm'; @@ -49,6 +53,8 @@ export class Config */ updateConfig( data: any ) { this.exhortSnykToken = data.redHatDependencyAnalytics.exhortSnykToken; + this.exhortOSSIndexUser = data.redHatDependencyAnalytics.exhortOSSIndexUser; + this.exhortOSSIndexToken = data.redHatDependencyAnalytics.exhortOSSIndexToken; this.matchManifestVersions = data.redHatDependencyAnalytics.matchManifestVersions ? 'true' : 'false'; this.exhortMvnPath = data.mvn.executable.path || 'mvn'; this.exhortNpmPath = data.npm.executable.path || 'npm'; diff --git a/src/diagnosticsHandler.ts b/src/diagnosticsHandler.ts index cef96b6c..8deedc11 100644 --- a/src/diagnosticsHandler.ts +++ b/src/diagnosticsHandler.ts @@ -4,7 +4,7 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -import { Diagnostic, CodeAction } from 'vscode-languageserver'; +import { Diagnostic } from 'vscode-languageserver'; import { DependencyMap, IDependencyProvider } from './collector'; import { componentAnalysisService, DependencyData } from './componentAnalysis'; import { Vulnerability } from './vulnerability'; diff --git a/src/server.ts b/src/server.ts index fe99e1c4..dd8fb142 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,6 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -import * as path from 'path'; import { TextDocumentSyncKind, Connection, DidChangeConfigurationNotification } from 'vscode-languageserver'; import { createConnection, TextDocuments, InitializeResult, CodeAction, ProposedFeatures } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument';