From f1476a710d450c424fe48adba8dd59222b75d7b5 Mon Sep 17 00:00:00 2001 From: daz Date: Wed, 10 Apr 2024 20:43:44 -0600 Subject: [PATCH 1/4] Initial import of wrapper-validation-action --- sources/package-lock.json | 328 ++++-- sources/package.json | 13 +- sources/src/wrapper-validation/checksums.ts | 55 + sources/src/wrapper-validation/find.ts | 27 + sources/src/wrapper-validation/hash.ts | 18 + sources/src/wrapper-validation/main.ts | 38 + sources/src/wrapper-validation/validate.ts | 98 ++ .../wrapper-validation/wrapper-checksums.json | 1014 +++++++++++++++++ .../jest/wrapper-validation/checksums.test.ts | 55 + .../data/invalid/gradle-wrapper.jar | 0 .../data/invalid/gradlе-wrapper.jar | 0 .../data/valid/gradle-wrapper.jar | Bin 0 -> 58798 bytes .../test/jest/wrapper-validation/find.test.ts | 12 + .../test/jest/wrapper-validation/hash.test.ts | 12 + .../jest/wrapper-validation/validate.test.ts | 98 ++ sources/tsconfig.json | 7 +- wrapper-validation/action.yml | 29 + 17 files changed, 1709 insertions(+), 95 deletions(-) create mode 100644 sources/src/wrapper-validation/checksums.ts create mode 100644 sources/src/wrapper-validation/find.ts create mode 100644 sources/src/wrapper-validation/hash.ts create mode 100644 sources/src/wrapper-validation/main.ts create mode 100644 sources/src/wrapper-validation/validate.ts create mode 100644 sources/src/wrapper-validation/wrapper-checksums.json create mode 100644 sources/test/jest/wrapper-validation/checksums.test.ts create mode 100644 sources/test/jest/wrapper-validation/data/invalid/gradle-wrapper.jar create mode 100644 sources/test/jest/wrapper-validation/data/invalid/gradlе-wrapper.jar create mode 100644 sources/test/jest/wrapper-validation/data/valid/gradle-wrapper.jar create mode 100644 sources/test/jest/wrapper-validation/find.test.ts create mode 100644 sources/test/jest/wrapper-validation/hash.test.ts create mode 100644 sources/test/jest/wrapper-validation/validate.test.ts create mode 100644 wrapper-validation/action.yml diff --git a/sources/package-lock.json b/sources/package-lock.json index 04e209c..f24ad98 100644 --- a/sources/package-lock.json +++ b/sources/package-lock.json @@ -21,7 +21,9 @@ "@octokit/rest": "20.1.0", "@octokit/webhooks-types": "7.5.0", "semver": "7.6.0", - "string-argv": "0.3.2" + "string-argv": "0.3.2", + "typed-rest-client": "1.8.11", + "unhomoglyph": "1.0.6" }, "devDependencies": { "@types/jest": "29.5.12", @@ -33,8 +35,10 @@ "eslint-plugin-github": "4.10.2", "eslint-plugin-jest": "27.9.0", "eslint-plugin-prettier": "5.1.3", + "glob-parent": "6.0.2", "jest": "29.7.0", "js-yaml": "4.1.0", + "nock": "13.5.4", "npm-run-all": "4.1.5", "patch-package": "8.0.0", "prettier": "3.2.5", @@ -3419,14 +3423,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3774,17 +3782,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3974,6 +3984,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -4828,7 +4857,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4879,16 +4907,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5015,7 +5046,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -5065,12 +5095,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5080,7 +5109,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5092,7 +5120,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5119,7 +5146,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6315,6 +6341,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6757,6 +6789,20 @@ "tslib": "^2.0.3" } }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-fetch": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", @@ -6996,7 +7042,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7532,6 +7577,15 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7557,6 +7611,20 @@ } ] }, + "node_modules/qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7872,15 +7940,16 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7931,14 +8000,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8624,6 +8696,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "node_modules/typescript": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", @@ -8652,6 +8734,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/undici": { "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", @@ -8668,6 +8755,11 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unhomoglyph": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" + }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -11601,14 +11693,15 @@ "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" }, "call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -11857,14 +11950,13 @@ "dev": true }, "define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, "define-properties": { @@ -12015,6 +12107,19 @@ "which-typed-array": "^1.1.13" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -12649,8 +12754,7 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.6", @@ -12683,11 +12787,11 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -12774,7 +12878,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -12812,25 +12915,22 @@ "dev": true }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" } }, "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", @@ -12845,7 +12945,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "requires": { "function-bind": "^1.1.2" } @@ -13714,6 +13813,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -14086,6 +14191,17 @@ "tslib": "^2.0.3" } }, + "nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, "node-fetch": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", @@ -14267,8 +14383,7 @@ "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, "object-is": { "version": "1.1.5", @@ -14644,6 +14759,12 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -14656,6 +14777,14 @@ "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", "dev": true }, + "qs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", + "requires": { + "side-channel": "^1.0.6" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14870,15 +14999,16 @@ } }, "set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "requires": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" } }, "set-function-name": { @@ -14914,14 +15044,14 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -15418,6 +15548,16 @@ "is-typed-array": "^1.1.9" } }, + "typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "requires": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "typescript": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", @@ -15436,6 +15576,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "undici": { "version": "5.28.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", @@ -15449,6 +15594,11 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "unhomoglyph": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" + }, "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", diff --git a/sources/package.json b/sources/package.json index 3985d5d..e5e4b70 100644 --- a/sources/package.json +++ b/sources/package.json @@ -12,6 +12,7 @@ "compile-dependency-submission-post": "ncc build src/dependency-submission/post.ts --out dist/dependency-submission/post --source-map --no-source-map-register", "compile-setup-gradle-main": "ncc build src/setup-gradle/main.ts --out dist/setup-gradle/main --source-map --no-source-map-register", "compile-setup-gradle-post": "ncc build src/setup-gradle/post.ts --out dist/setup-gradle/post --source-map --no-source-map-register", + "compile-wrapper-validation-main": "ncc build src/wrapper-validation/main.ts --out dist/wrapper-validation/main --source-map --no-source-map-register", "compile": "npm-run-all --parallel compile-*", "check": "npm-run-all --parallel prettier-check lint", "format": "npm-run-all --parallel prettier-write lint", @@ -42,8 +43,11 @@ "@octokit/rest": "20.1.0", "@octokit/webhooks-types": "7.5.0", "semver": "7.6.0", - "string-argv": "0.3.2" - }, + "string-argv": "0.3.2", + + "typed-rest-client": "1.8.11", + "unhomoglyph": "1.0.6" +}, "devDependencies": { "@types/jest": "29.5.12", "@types/node": "20.12.4", @@ -60,6 +64,9 @@ "patch-package": "8.0.0", "prettier": "3.2.5", "ts-jest": "29.1.2", - "typescript": "5.4.3" + "typescript": "5.4.3", + + "glob-parent": "6.0.2", + "nock": "13.5.4" } } diff --git a/sources/src/wrapper-validation/checksums.ts b/sources/src/wrapper-validation/checksums.ts new file mode 100644 index 0000000..4220b0c --- /dev/null +++ b/sources/src/wrapper-validation/checksums.ts @@ -0,0 +1,55 @@ +import * as httpm from 'typed-rest-client/HttpClient' + +import fileWrapperChecksums from './wrapper-checksums.json' + +const httpc = new httpm.HttpClient('gradle/wrapper-validation-action', undefined, {allowRetries: true, maxRetries: 3}) + +function getKnownValidChecksums(): Map> { + const versionsMap = new Map>() + for (const entry of fileWrapperChecksums) { + const checksum = entry.checksum + + let versionNames = versionsMap.get(checksum) + if (versionNames === undefined) { + versionNames = new Set() + versionsMap.set(checksum, versionNames) + } + + versionNames.add(entry.version) + } + + return versionsMap +} + +/** + * Known checksums from previously published Wrapper versions. + * + * Maps from the checksum to the names of the Gradle versions whose wrapper has this checksum. + */ +export const KNOWN_VALID_CHECKSUMS = getKnownValidChecksums() + +export async function fetchValidChecksums(allowSnapshots: boolean): Promise> { + const all = await httpGetJsonArray('https://services.gradle.org/versions/all') + const withChecksum = all.filter( + entry => typeof entry === 'object' && entry != null && entry.hasOwnProperty('wrapperChecksumUrl') + ) + const allowed = withChecksum.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (entry: any) => allowSnapshots || !entry.snapshot + ) + const checksumUrls = allowed.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (entry: any) => entry.wrapperChecksumUrl as string + ) + const checksums = await Promise.all(checksumUrls.map(async (url: string) => httpGetText(url))) + return new Set(checksums) +} + +async function httpGetJsonArray(url: string): Promise { + return JSON.parse(await httpGetText(url)) +} + +async function httpGetText(url: string): Promise { + const response = await httpc.get(url) + return await response.readBody() +} diff --git a/sources/src/wrapper-validation/find.ts b/sources/src/wrapper-validation/find.ts new file mode 100644 index 0000000..de1a89e --- /dev/null +++ b/sources/src/wrapper-validation/find.ts @@ -0,0 +1,27 @@ +import * as util from 'util' +import * as path from 'path' +import * as fs from 'fs' +import unhomoglyph from 'unhomoglyph' + +const readdir = util.promisify(fs.readdir) + +export async function findWrapperJars(baseDir: string): Promise { + const files = await recursivelyListFiles(baseDir) + return files + .filter(file => unhomoglyph(file).endsWith('gradle-wrapper.jar')) + .map(wrapperJar => path.relative(baseDir, wrapperJar)) + .sort((a, b) => a.localeCompare(b)) +} + +async function recursivelyListFiles(baseDir: string): Promise { + const childrenNames = await readdir(baseDir) + const childrenPaths = await Promise.all( + childrenNames.map(async childName => { + const childPath = path.resolve(baseDir, childName) + return fs.lstatSync(childPath).isDirectory() + ? recursivelyListFiles(childPath) + : new Promise(resolve => resolve([childPath])) + }) + ) + return Array.prototype.concat(...childrenPaths) +} diff --git a/sources/src/wrapper-validation/hash.ts b/sources/src/wrapper-validation/hash.ts new file mode 100644 index 0000000..90339e1 --- /dev/null +++ b/sources/src/wrapper-validation/hash.ts @@ -0,0 +1,18 @@ +import * as crypto from 'crypto' +import * as fs from 'fs' + +export async function sha256File(path: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256') + const stream = fs.createReadStream(path) + stream.on('data', data => hash.update(data)) + stream.on('end', () => { + stream.destroy() + resolve(hash.digest('hex')) + }) + stream.on('error', error => { + stream.destroy() + reject(error) + }) + }) +} diff --git a/sources/src/wrapper-validation/main.ts b/sources/src/wrapper-validation/main.ts new file mode 100644 index 0000000..9fb3070 --- /dev/null +++ b/sources/src/wrapper-validation/main.ts @@ -0,0 +1,38 @@ +import * as path from 'path' +import * as core from '@actions/core' + +import * as validate from './validate' + +export async function run(): Promise { + try { + const result = await validate.findInvalidWrapperJars( + path.resolve('.'), + +core.getInput('min-wrapper-count'), + core.getInput('allow-snapshots') === 'true', + core.getInput('allow-checksums').split(',') + ) + if (result.isValid()) { + core.info(result.toDisplayString()) + } else { + core.setFailed( + `Gradle Wrapper Validation Failed!\n See https://github.com/gradle/wrapper-validation-action#reporting-failures\n${result.toDisplayString()}` + ) + if (result.invalid.length > 0) { + core.setOutput('failed-wrapper', `${result.invalid.map(w => w.path).join('|')}`) + } + } + } catch (error) { + if (error instanceof AggregateError) { + core.setFailed(`Multiple errors returned`) + for (const err of error.errors) { + core.error(`Error ${error.errors.indexOf(err)}: ${err.message}`) + } + } else if (error instanceof Error) { + core.setFailed(error.message) + } else { + core.setFailed(`Unknown object was thrown: ${error}`) + } + } +} + +run() diff --git a/sources/src/wrapper-validation/validate.ts b/sources/src/wrapper-validation/validate.ts new file mode 100644 index 0000000..add8e83 --- /dev/null +++ b/sources/src/wrapper-validation/validate.ts @@ -0,0 +1,98 @@ +import * as find from './find' +import * as checksums from './checksums' +import * as hash from './hash' +import {resolve} from 'path' + +export async function findInvalidWrapperJars( + gitRepoRoot: string, + minWrapperCount: number, + allowSnapshots: boolean, + allowedChecksums: string[], + knownValidChecksums: Map> = checksums.KNOWN_VALID_CHECKSUMS +): Promise { + const wrapperJars = await find.findWrapperJars(gitRepoRoot) + const result = new ValidationResult([], []) + if (wrapperJars.length < minWrapperCount) { + result.errors.push( + `Expected to find at least ${minWrapperCount} Gradle Wrapper JARs but got only ${wrapperJars.length}` + ) + } + if (wrapperJars.length > 0) { + const notYetValidatedWrappers = [] + for (const wrapperJar of wrapperJars) { + const sha = await hash.sha256File(resolve(gitRepoRoot, wrapperJar)) + if (allowedChecksums.includes(sha) || knownValidChecksums.has(sha)) { + result.valid.push(new WrapperJar(wrapperJar, sha)) + } else { + notYetValidatedWrappers.push(new WrapperJar(wrapperJar, sha)) + } + } + + // Otherwise fall back to fetching checksums from Gradle API and compare against them + if (notYetValidatedWrappers.length > 0) { + result.fetchedChecksums = true + const fetchedValidChecksums = await checksums.fetchValidChecksums(allowSnapshots) + + for (const wrapperJar of notYetValidatedWrappers) { + if (!fetchedValidChecksums.has(wrapperJar.checksum)) { + result.invalid.push(wrapperJar) + } else { + result.valid.push(wrapperJar) + } + } + } + } + return result +} + +export class ValidationResult { + valid: WrapperJar[] + invalid: WrapperJar[] + fetchedChecksums = false + errors: string[] = [] + + constructor(valid: WrapperJar[], invalid: WrapperJar[]) { + this.valid = valid + this.invalid = invalid + } + + isValid(): boolean { + return this.invalid.length === 0 && this.errors.length === 0 + } + + toDisplayString(): string { + let displayString = '' + if (this.invalid.length > 0) { + displayString += `✗ Found unknown Gradle Wrapper JAR files:\n${ValidationResult.toDisplayList( + this.invalid + )}` + } + if (this.errors.length > 0) { + if (displayString.length > 0) displayString += '\n' + displayString += `✗ Other validation errors:\n ${this.errors.join(`\n `)}` + } + if (this.valid.length > 0) { + if (displayString.length > 0) displayString += '\n' + displayString += `✓ Found known Gradle Wrapper JAR files:\n${ValidationResult.toDisplayList(this.valid)}` + } + return displayString + } + + private static toDisplayList(wrapperJars: WrapperJar[]): string { + return ` ${wrapperJars.map(wj => wj.toDisplayString()).join(`\n `)}` + } +} + +export class WrapperJar { + path: string + checksum: string + + constructor(path: string, checksum: string) { + this.path = path + this.checksum = checksum + } + + toDisplayString(): string { + return `${this.checksum} ${this.path}` + } +} diff --git a/sources/src/wrapper-validation/wrapper-checksums.json b/sources/src/wrapper-validation/wrapper-checksums.json new file mode 100644 index 0000000..f159020 --- /dev/null +++ b/sources/src/wrapper-validation/wrapper-checksums.json @@ -0,0 +1,1014 @@ +[ + { + "version": "8.7", + "checksum": "cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8" + }, + { + "version": "8.7-rc-4", + "checksum": "cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8" + }, + { + "version": "8.7-rc-3", + "checksum": "cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8" + }, + { + "version": "8.7-rc-2", + "checksum": "cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8" + }, + { + "version": "8.7-rc-1", + "checksum": "cb0da6751c2b753a16ac168bb354870ebb1e162e9083f116729cec9c781156b8" + }, + { + "version": "7.6.4", + "checksum": "14dfa961b6704bb3decdea06502781edaa796a82e6da41cd2e1962b14fbe21a3" + }, + { + "version": "8.6", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.6-rc-4", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.6-rc-3", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.6-rc-2", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.6-rc-1", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.6-milestone-1", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.5", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.5-rc-4", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.5-rc-3", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.5-rc-2", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.5-rc-1", + "checksum": "d3b261c2820e9e3d8d639ed084900f11f4a86050a8f83342ade7b6bc9b0d2bdd" + }, + { + "version": "8.4", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "7.6.3", + "checksum": "14dfa961b6704bb3decdea06502781edaa796a82e6da41cd2e1962b14fbe21a3" + }, + { + "version": "8.4-rc-3", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.4-rc-2", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.4-rc-1", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.3", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.3-rc-4", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.3-rc-3", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.3-rc-2", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.3-rc-1", + "checksum": "0336f591bc0ec9aa0c9988929b93ecc916b3c1d52aed202c7381db144aa0ef15" + }, + { + "version": "8.2.1", + "checksum": "a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4" + }, + { + "version": "8.2", + "checksum": "a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4" + }, + { + "version": "7.6.2", + "checksum": "14dfa961b6704bb3decdea06502781edaa796a82e6da41cd2e1962b14fbe21a3" + }, + { + "version": "8.2-rc-3", + "checksum": "a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4" + }, + { + "version": "8.2-rc-2", + "checksum": "5c9a1a6f50b4f8c0264b1ac69013bef9f8363733275fafa56c70c84be3276bb8" + }, + { + "version": "8.2-rc-1", + "checksum": "55e949185c26ba3ddcd2c6a4217d043bfa0ce3cc002bbbb52b709a181a513e81" + }, + { + "version": "8.2-milestone-1", + "checksum": "55e949185c26ba3ddcd2c6a4217d043bfa0ce3cc002bbbb52b709a181a513e81" + }, + { + "version": "8.1.1", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.1", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.1-rc-4", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.1-rc-3", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.1-rc-2", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.1-rc-1", + "checksum": "ed2c26eba7cfb93cc2b7785d05e534f07b5b48b5e7fc941921cd098628abca58" + }, + { + "version": "8.0.2", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "7.6.1", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "6.9.4", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "8.0.1", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-rc-5", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-rc-4", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-rc-3", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-rc-2", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-rc-1", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-milestone-6", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-milestone-5", + "checksum": "91941f522fbfd4431cf57e445fc3d5200c85f957bda2de5251353cf11174f4b5" + }, + { + "version": "8.0-milestone-4", + "checksum": "577b2de036000db2e0f04f2ec842a4f1e648c8b6f9c87f29a8d896acb1732538" + }, + { + "version": "7.6", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.6-rc-4", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.6-rc-3", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.6-rc-2", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "8.0-milestone-3", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.6-rc-1", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "6.9.3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "8.0-milestone-2", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "8.0-milestone-1", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.6-milestone-1", + "checksum": "c5a643cf80162e665cc228f7b16f343fef868e47d3a4836f62e18b7e17ac018a" + }, + { + "version": "7.5.1", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5-rc-5", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5-rc-4", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5-rc-3", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5-rc-2", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.5-rc-1", + "checksum": "91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60" + }, + { + "version": "7.4.2", + "checksum": "575098db54a998ff1c6770b352c3b16766c09848bee7555dab09afc34e8cf590" + }, + { + "version": "7.4.1", + "checksum": "575098db54a998ff1c6770b352c3b16766c09848bee7555dab09afc34e8cf590" + }, + { + "version": "7.4", + "checksum": "575098db54a998ff1c6770b352c3b16766c09848bee7555dab09afc34e8cf590" + }, + { + "version": "7.4-rc-2", + "checksum": "575098db54a998ff1c6770b352c3b16766c09848bee7555dab09afc34e8cf590" + }, + { + "version": "7.4-rc-1", + "checksum": "575098db54a998ff1c6770b352c3b16766c09848bee7555dab09afc34e8cf590" + }, + { + "version": "7.3.3", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3.3-rc-1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "6.9.2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.3.2", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3.1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3-rc-5", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3-rc-4", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3-rc-3", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3-rc-2", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.3-rc-1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "6.9.1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.2", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.2-rc-3", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.2-rc-2", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.2-rc-1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.1.1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.1-rc-2", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.1-rc-1", + "checksum": "33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89" + }, + { + "version": "7.0.2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0.1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.9", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.9-rc-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.9-rc-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0-rc-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0-rc-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0-milestone-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0-milestone-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8.3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8.2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "7.0-milestone-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8.1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-rc-5", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-rc-4", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-rc-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-rc-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7.1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-milestone-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-milestone-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.8-milestone-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7-rc-5", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7-rc-4", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7-rc-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7-rc-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.7-rc-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6.1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-6", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-5", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-4", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-rc-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-milestone-3", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.5.1", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.6-milestone-2", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.6-milestone-1", + "checksum": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637" + }, + { + "version": "6.5", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.5-rc-1", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4.1", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.5-milestone-2", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.5-milestone-1", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4-rc-4", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4-rc-3", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4-rc-2", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.4-rc-1", + "checksum": "70239e6ca1f0d5e3b2808ef6d82390cf9ad58d3a3a0d271677a51d1b89475857" + }, + { + "version": "6.3", + "checksum": "1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06" + }, + { + "version": "6.3-rc-4", + "checksum": "1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06" + }, + { + "version": "6.3-rc-3", + "checksum": "1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06" + }, + { + "version": "6.3-rc-2", + "checksum": "1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06" + }, + { + "version": "6.3-rc-1", + "checksum": "1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06" + }, + { + "version": "6.2.2", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.2.1", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.2", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.2-rc-3", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.2-rc-2", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.2-rc-1", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1.1", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1-rc-3", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1-rc-2", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1-rc-1", + "checksum": "96f793a18e056c23ffeec67c1f3bb8eccff5a4a407fc9ceac183527e7eedf4b6" + }, + { + "version": "6.1-milestone-3", + "checksum": "3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce" + }, + { + "version": "6.1-milestone-2", + "checksum": "3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce" + }, + { + "version": "6.1-milestone-1", + "checksum": "3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce" + }, + { + "version": "6.0.1", + "checksum": "28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e" + }, + { + "version": "6.0", + "checksum": "28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e" + }, + { + "version": "6.0-rc-3", + "checksum": "28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e" + }, + { + "version": "5.6.4", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "6.0-rc-2", + "checksum": "28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e" + }, + { + "version": "6.0-rc-1", + "checksum": "28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e" + }, + { + "version": "5.6.3", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.6.2", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.6.1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.6", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.6-rc-2", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.6-rc-1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5.1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5-rc-4", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5-rc-3", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5-rc-2", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.5-rc-1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.4.1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.4", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.4-rc-1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.3.1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.3", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.3-rc-3", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.3-rc-2", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.3-rc-1", + "checksum": "3dc39ad650d40f6c029bd8ff605c6d95865d657dbfdeacdb079db0ddfffedf9f" + }, + { + "version": "5.2.1", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.2", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.2-rc-1", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.1.1", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.1", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.1-rc-3", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.1-rc-2", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "5.1-rc-1", + "checksum": "76b12da7f4a7cdd025e5996811a2e49bf5df0fb62d72554ab555c0e434b63aae" + }, + { + "version": "4.10.3", + "checksum": "660ab018b8e319e9ae779fdb1b7ac47d0321bde953bf0eb4545f14952cfdcaa3" + }, + { + "version": "5.1-milestone-1", + "checksum": "8ff6bee43c55efc0cce9e1147860a76fc970398fbef587e64b6e7a5a7e0291df" + }, + { + "version": "5.0", + "checksum": "f1a597a1f2b23089deec11d5b924d074f9e4ed810f2093be7021ded01c8073ad" + }, + { + "version": "4.10.2", + "checksum": "ad63ba21fb91e490e0f6fd0ca7d4049241f0f68a454b0b3075c041c4554e611c" + }, + { + "version": "4.10.1", + "checksum": "d8a69ca8efe271d8de080c42a2ea4b08fc9e85c41aa2d163255c70d9da239db0" + }, + { + "version": "4.10", + "checksum": "778e7f46bd67eaea2de5bcbdbb40878c6614656014ba59a72ce8648eaf43a925" + }, + { + "version": "4.9", + "checksum": "e55e7e47a79e04c26363805b31e2f40b7a9cc89ea12113be7de750a3b2cede85" + }, + { + "version": "4.8.1", + "checksum": "080e30657661539701b66827b96eb0043191e0a7a73090e8a57bd6735e5af5c5" + }, + { + "version": "4.8", + "checksum": "4160d5a6d8d6efc6af336582bbbba8194e4d7a742835f7b0fd3964cbd419c994" + }, + { + "version": "4.7", + "checksum": "7d6fe0a055f133226409de2457fb0e887d1f6b096f36c8d5fd76fb7a9357cd45" + }, + { + "version": "4.6", + "checksum": "381dff8aa434499aa93bc25572b049c8c586a67faff2c02f375e4f23e17e49de" + }, + { + "version": "4.5.1", + "checksum": "3bf04e39ed259ff0a1217a875199a11775855d2a29207b98318ac79178249de8" + }, + { + "version": "4.5", + "checksum": "d283a04caee0e97b666b09e50ab394834934bee6225c2e9cd4650afdeac43828" + }, + { + "version": "4.4.1", + "checksum": "4e318d74d06aa7b998091345c397a3c7c4b291b59da31e6f9c772a596711acac" + }, + { + "version": "4.4", + "checksum": "88b5b31f390a268ab3773df580d83fd1e388f49c2b685f78a16600577bd72fe2" + }, + { + "version": "4.3.1", + "checksum": "383f4efa709b52632a520708e8a07353961970941ab3867ab8ac182132ce1c54" + }, + { + "version": "4.3", + "checksum": "ba496e7e0e03ffa432eaf715bb1466fac2ffc8491a71e7164a5438c48c79d8ea" + }, + { + "version": "4.2.1", + "checksum": "c536e519e65a4bb787e071c6a90d23bd219c9d409a2123db649d3684acbf3ae7" + }, + { + "version": "4.2", + "checksum": "53aa048fef3c06a8442c6a44df5edd2e8c791ee883e42ded6189c7eed112095d" + }, + { + "version": "4.1", + "checksum": "f4d953f31fbf6c38a8c330d19171c8ba6e0d1ff59d4d5c5c2d3ed821c9f3d5a3" + }, + { + "version": "3.2.1", + "checksum": "020ef0245a07b33ca48b12f59415e7e5083cf701ef02690464a8cc2ab3984608" + }, + { + "version": "3.2", + "checksum": "d2d3abae74e89cc4200f48d4a08a7e5960363c33ee62272ef5ffbb39f4c7f83e" + }, + { + "version": "3.1", + "checksum": "0f49043be582d7a39b671f924c66bd9337b92fa88ff5951225acc60560053067" + }, + { + "version": "3.0", + "checksum": "42d7a2f636983aa09d21dfeb6e90d21d7a8dad905351390643ce60cc82c8f8a9" + }, + { + "version": "2.14.1", + "checksum": "8e47da0b2656354d059609cae9d44b196d4f9b14512e688ffee4e0eb7e723ae9" + }, + { + "version": "2.14", + "checksum": "b5ca811c057b3eb4164c78f4155d667c6092ff98ba91a4c90d29e127426f37a7" + }, + { + "version": "2.13", + "checksum": "0c3c576e28b44eddcab6b8b4854f484363dfbcd047657d41654e839835da2c53" + }, + { + "version": "2.12", + "checksum": "4894520b03c007bf38e983bf933320c483a9790010d997029fa8985dc6128559" + }, + { + "version": "2.11", + "checksum": "0bc7b16a0a3fa52af674de44d1fea48abc4dee3431f3d4829cd9ea329836e596" + }, + { + "version": "2.10", + "checksum": "16caeaf66d57a0d1d2087fef6a97efa62de8da69afa5b908f40db35afc4342da" + }, + { + "version": "2.9", + "checksum": "b92386e36a96da6be89e91f71087d1394a26c0450231ba0b22e28ee1ee8fa14b" + }, + { + "version": "2.8", + "checksum": "198159fcd7d29533c0d37423d66c44729982d5280c9e2c7c5f4b7bc6a9317f6b" + }, + { + "version": "2.7", + "checksum": "d7e1975ccf2dc079d4f0b1010febdad466506d1565c5aa8017c88ebc5e471604" + }, + { + "version": "2.6", + "checksum": "695089a2b306f55f0bd63140fbcc5ead8c383819018188ce484cd5a055bec6e4" + }, + { + "version": "2.5", + "checksum": "718d7b25ea60b357fc4cb2212ce10b3f03dfd0e6fe5f23f565b15553ec46bb7e" + }, + { + "version": "2.4", + "checksum": "98420079ffe3e24b1013180d9b9bc2e2ee6a9d867ee232004b75a961d9c18e27" + }, + { + "version": "2.3", + "checksum": "b18a1114ebe81fb7502d40ad9a4f86cef82fff244a865ad45533b4d5e7ff0cc8" + }, + { + "version": "2.2.1", + "checksum": "5f73d431fd1c5dcc2cf11555b8e486c43249c1099f678ccc6088b05be600a2e1" + }, + { + "version": "2.2", + "checksum": "fa9b4294d47cf8db7039cb9b2435de3dd1accb0d3d67926705775a0579dfa397" + }, + { + "version": "2.1", + "checksum": "5e27c39c2336c25748f279d8b105162d14b1a39eb7839d0b658432282d0ce79f" + }, + { + "version": "2.0", + "checksum": "80a33ca14e3bca3116bc8749550397f739f126190c82bb6399fdc8d10f49661f" + }, + { + "version": "1.12", + "checksum": "dea5ceba47b58df0b7f69a65b24357527c1927ccc72b6d4ed90658d39e461b29" + }, + { + "version": "1.11", + "checksum": "a14b54dd3790f5ce1dc08ebbf4b5bcc05f76c4554b43accb84696c970f29aba0" + }, + { + "version": "1.10", + "checksum": "6a6c15e222a0458aa33985b87f67954f4222410b43b1e26866197d0a77d93cbc" + }, + { + "version": "1.9", + "checksum": "134337ea7c13221f9d1a1c14288a5cf8af9f6060167b903b724b115cf5a0cf73" + }, + { + "version": "1.8", + "checksum": "13f23a24252ddca0a0fabef212e3c854f5895b081c09d015c91587a5df9bf9f7" + }, + { + "version": "1.7", + "checksum": "7af529cc3331d38b3d8f8344ddd9b2d3744542b55b68318abd8bb1a6f3812a1c" + }, + { + "version": "1.6", + "checksum": "66dbcc9f0bc33789ea0bdb4d49c8ea037047bda5647ef696c47bdca65f785159" + }, + { + "version": "1.5", + "checksum": "9598ffdf7ee26949d8b861ece267c70c802f21f7fc52596693834792d155195a" + }, + { + "version": "1.4", + "checksum": "c95985b7b5684e133c5d45044fd90faaf6c8f7cd2493d61a11c2b8c5b71ef514" + }, + { + "version": "1.3", + "checksum": "95513eccca99e1ae1aeadc4f69cabd0e7fb64821d3f26c46a489df844c8a7353" + }, + { + "version": "1.2", + "checksum": "5c91fa893665f3051eae14578fac2df14e737423387e75ffbeccd35f335a3d8b" + }, + { + "version": "1.1", + "checksum": "22c56a9780daeee00e5bf31621f991b68e73eff6fe8afca628a1fe2c50c6038e" + }, + { + "version": "1.0", + "checksum": "87e50531ca7aab675f5bb65755ef78328afd64cf0877e37ad876047a8a014055" + } +] diff --git a/sources/test/jest/wrapper-validation/checksums.test.ts b/sources/test/jest/wrapper-validation/checksums.test.ts new file mode 100644 index 0000000..ce5f750 --- /dev/null +++ b/sources/test/jest/wrapper-validation/checksums.test.ts @@ -0,0 +1,55 @@ +import * as checksums from '../../../src/wrapper-validation/checksums' +import nock from 'nock' +import {afterEach, describe, expect, test, jest} from '@jest/globals' + +jest.setTimeout(30000) + +test('has loaded hardcoded wrapper jars checksums', async () => { + // Sanity check that generated checksums file is not empty and was properly imported + expect(checksums.KNOWN_VALID_CHECKSUMS.size).toBeGreaterThan(10) + // Verify that checksums of arbitrary versions are contained + expect( + checksums.KNOWN_VALID_CHECKSUMS.get( + '660ab018b8e319e9ae779fdb1b7ac47d0321bde953bf0eb4545f14952cfdcaa3' + ) + ).toEqual(new Set(['4.10.3'])) + expect( + checksums.KNOWN_VALID_CHECKSUMS.get( + '28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e' + ) + ).toEqual(new Set(['6.0-rc-1', '6.0-rc-2', '6.0-rc-3', '6.0', '6.0.1'])) +}) + +test('fetches wrapper jars checksums', async () => { + const validChecksums = await checksums.fetchValidChecksums(false) + expect(validChecksums.size).toBeGreaterThan(10) + // Verify that checksum of arbitrary version is contained + expect( + validChecksums.has( + // Checksum for version 6.0 + '28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e' + ) + ).toBe(true) +}) + +describe('retry', () => { + afterEach(() => { + nock.cleanAll() + }) + + describe('for /versions/all API', () => { + test('retry three times', async () => { + nock('https://services.gradle.org', {allowUnmocked: true}) + .get('/versions/all') + .times(3) + .replyWithError({ + message: 'connect ECONNREFUSED 104.18.191.9:443', + code: 'ECONNREFUSED' + }) + + const validChecksums = await checksums.fetchValidChecksums(false) + expect(validChecksums.size).toBeGreaterThan(10) + nock.isDone() + }) + }) +}) diff --git a/sources/test/jest/wrapper-validation/data/invalid/gradle-wrapper.jar b/sources/test/jest/wrapper-validation/data/invalid/gradle-wrapper.jar new file mode 100644 index 0000000..e69de29 diff --git a/sources/test/jest/wrapper-validation/data/invalid/gradlе-wrapper.jar b/sources/test/jest/wrapper-validation/data/invalid/gradlе-wrapper.jar new file mode 100644 index 0000000..e69de29 diff --git a/sources/test/jest/wrapper-validation/data/valid/gradle-wrapper.jar b/sources/test/jest/wrapper-validation/data/valid/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..6d183c6b6f1b37f3fb960b68d8b1386b3e010383 GIT binary patch literal 58798 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 { + const repoRoot = path.resolve('./test/jest/wrapper-validation') + const wrapperJars = await find.findWrapperJars(repoRoot) + expect(wrapperJars.length).toBe(3) + expect(wrapperJars).toContain('data/valid/gradle-wrapper.jar') + expect(wrapperJars).toContain('data/invalid/gradle-wrapper.jar') + expect(wrapperJars).toContain('data/invalid/gradlе-wrapper.jar') // homoglyph +}) diff --git a/sources/test/jest/wrapper-validation/hash.test.ts b/sources/test/jest/wrapper-validation/hash.test.ts new file mode 100644 index 0000000..929f75a --- /dev/null +++ b/sources/test/jest/wrapper-validation/hash.test.ts @@ -0,0 +1,12 @@ +import * as path from 'path' +import * as hash from '../../../src/wrapper-validation/hash' +import {expect, test} from '@jest/globals' + +test('can sha256 files', async () => { + const sha = await hash.sha256File( + path.resolve('test/jest/wrapper-validation/data/invalid/gradle-wrapper.jar') + ) + expect(sha).toEqual( + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ) +}) diff --git a/sources/test/jest/wrapper-validation/validate.test.ts b/sources/test/jest/wrapper-validation/validate.test.ts new file mode 100644 index 0000000..5aacd10 --- /dev/null +++ b/sources/test/jest/wrapper-validation/validate.test.ts @@ -0,0 +1,98 @@ +import * as path from 'path' +import * as validate from '../../../src/wrapper-validation/validate' +import {expect, test, jest} from '@jest/globals' + +jest.setTimeout(30000) + +const baseDir = path.resolve('./test/jest/wrapper-validation') + +test('succeeds if all found wrapper jars are valid', async () => { + const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [ + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ]) + + expect(result.isValid()).toBe(true) + // Only hardcoded and explicitly allowed checksums should have been used + expect(result.fetchedChecksums).toBe(false) + + expect(result.toDisplayString()).toBe( + '✓ Found known Gradle Wrapper JAR files:\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph + ' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar' + ) +}) + +test('succeeds if all found wrapper jars are valid (and checksums are fetched from Gradle API)', async () => { + const knownValidChecksums = new Map>() + const result = await validate.findInvalidWrapperJars( + baseDir, + 1, + false, + ['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'], + knownValidChecksums + ) + + expect(result.isValid()).toBe(true) + // Should have fetched checksums because no known checksums were provided + expect(result.fetchedChecksums).toBe(true) + + expect(result.toDisplayString()).toBe( + '✓ Found known Gradle Wrapper JAR files:\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph + ' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar' + ) +}) + +test('fails if invalid wrapper jars are found', async () => { + const result = await validate.findInvalidWrapperJars(baseDir, 3, false, []) + + expect(result.isValid()).toBe(false) + + expect(result.valid).toEqual([ + new validate.WrapperJar( + 'data/valid/gradle-wrapper.jar', + '3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce' + ) + ]) + + expect(result.invalid).toEqual([ + new validate.WrapperJar( + 'data/invalid/gradle-wrapper.jar', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ), + new validate.WrapperJar( + 'data/invalid/gradlе-wrapper.jar', // homoglyph + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ) + ]) + + expect(result.toDisplayString()).toBe( + '✗ Found unknown Gradle Wrapper JAR files:\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph + '✓ Found known Gradle Wrapper JAR files:\n' + + ' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar' + ) +}) + +test('fails if not enough wrapper jars are found', async () => { + const result = await validate.findInvalidWrapperJars(baseDir, 4, false, []) + + expect(result.isValid()).toBe(false) + + expect(result.errors).toEqual([ + 'Expected to find at least 4 Gradle Wrapper JARs but got only 3' + ]) + + expect(result.toDisplayString()).toBe( + '✗ Found unknown Gradle Wrapper JAR files:\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph + '✗ Other validation errors:\n' + + ' Expected to find at least 4 Gradle Wrapper JARs but got only 3\n' + + '✓ Found known Gradle Wrapper JAR files:\n' + + ' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar' + ) +}) diff --git a/sources/tsconfig.json b/sources/tsconfig.json index 583b426..7346688 100644 --- a/sources/tsconfig.json +++ b/sources/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { /* Basic Options */ - "incremental": false, /* Enable incremental compilation */ - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "incremental": false, /* Enable incremental compilation */ + "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ @@ -45,9 +45,10 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + "resolveJsonModule": true, /* Enable importing JSON files as module; used for importing wrapper checksums JSON */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ diff --git a/wrapper-validation/action.yml b/wrapper-validation/action.yml new file mode 100644 index 0000000..117fb54 --- /dev/null +++ b/wrapper-validation/action.yml @@ -0,0 +1,29 @@ +name: 'Gradle Wrapper Validation' +description: 'Validates Gradle Wrapper JAR Files' +author: 'Gradle' + +inputs: + min-wrapper-count: + description: 'Minimum number expected gradle-wrapper.jar files found in the repository. Non-negative number. Higher number is useful in monorepos where each project might have their own wrapper.' + required: false + default: '1' + allow-snapshots: + description: 'Allow Gradle snapshot versions during checksum verification. Boolean, true or false.' + required: false + default: 'false' + allow-checksums: + description: 'Accept arbitrary user-defined checksums as valid. Comma separated list of SHA256 checksums (lowercase hex).' + required: false + default: '' + +outputs: + failed-wrapper: + description: 'The path of the Gradle Wrapper(s) JAR that failed validation. Path is a platform-dependent relative path to git repository root. Multiple paths are separated by a | character.' + +runs: + using: 'node20' + main: '../dist/wrapper-validation/main/index.js' + +branding: + icon: 'shield' + color: gray-dark From 39cecc54d025e31136239d331b9f9cc19a7b250d Mon Sep 17 00:00:00 2001 From: daz Date: Wed, 10 Apr 2024 20:50:29 -0600 Subject: [PATCH 2/4] Add wrapper-validation workflows --- .github/workflows/ci-integ-test.yml | 4 + .../integ-test-wrapper-validation.yml | 68 ++++++++++++++ .github/workflows/update-checksums-file.js | 94 +++++++++++++++++++ .github/workflows/update-checksums-file.yml | 55 +++++++++++ 4 files changed, 221 insertions(+) create mode 100644 .github/workflows/integ-test-wrapper-validation.yml create mode 100644 .github/workflows/update-checksums-file.js create mode 100644 .github/workflows/update-checksums-file.yml diff --git a/.github/workflows/ci-integ-test.yml b/.github/workflows/ci-integ-test.yml index 8748c3b..b04db0b 100644 --- a/.github/workflows/ci-integ-test.yml +++ b/.github/workflows/ci-integ-test.yml @@ -179,3 +179,7 @@ jobs: uses: ./.github/workflows/integ-test-detect-java-toolchains.yml with: runner-os: '["ubuntu-latest"]' + + wrapper-validation: + needs: [determine-suite, build-distribution] + uses: ./.github/workflows/integ-test-wrapper-validation.yml diff --git a/.github/workflows/integ-test-wrapper-validation.yml b/.github/workflows/integ-test-wrapper-validation.yml new file mode 100644 index 0000000..ce1df32 --- /dev/null +++ b/.github/workflows/integ-test-wrapper-validation.yml @@ -0,0 +1,68 @@ +name: Test sample Kotlin DSL project + +on: + workflow_call: + +jobs: + # Integration test for successful validation of wrappers + test-validation-success: + name: 'Test: Validation success' + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Initialize integ-test + uses: ./.github/actions/init-integ-test + + - name: Run wrapper-validation-action + id: action-test + uses: ./wrapper-validation + with: + # to allow the invalid wrapper jar present in test data + allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + + - name: Check outcome + env: + # Evaluate workflow expressions here as env variable values instead of inside shell script + # below to not accidentally inject code into shell script or break its syntax + FAILED_WRAPPERS: ${{ steps.action-test.outputs.failed-wrapper }} + FAILED_WRAPPERS_MATCHES: ${{ steps.action-test.outputs.failed-wrapper == '' }} + run: | + if [ "$FAILED_WRAPPERS_MATCHES" != "true" ] ; then + echo "'outputs.failed-wrapper' has unexpected content: $FAILED_WRAPPERS" + exit 1 + fi + + # Integration test for failing validation of wrappers + test-validation-error: + name: 'Test: Validation error' + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Initialize integ-test + uses: ./.github/actions/init-integ-test + + - name: Run wrapper-validation-action + id: action-test + uses: ./wrapper-validation + # Expected to fail; validated below + continue-on-error: true + + - name: Check outcome + env: + # Evaluate workflow expressions here as env variable values instead of inside shell script + # below to not accidentally inject code into shell script or break its syntax + VALIDATION_FAILED: ${{ steps.action-test.outcome == 'failure' }} + FAILED_WRAPPERS: ${{ steps.action-test.outputs.failed-wrapper }} + FAILED_WRAPPERS_MATCHES: ${{ steps.action-test.outputs.failed-wrapper == 'sources/test/jest/wrapper-validation/data/invalid/gradle-wrapper.jar|sources/test/jest/wrapper-validation/data/invalid/gradlе-wrapper.jar' }} + run: | + if [ "$VALIDATION_FAILED" != "true" ] ; then + echo "Expected validation to fail, but it didn't" + exit 1 + fi + + if [ "$FAILED_WRAPPERS_MATCHES" != "true" ] ; then + echo "'outputs.failed-wrapper' has unexpected content: $FAILED_WRAPPERS" + exit 1 + fi diff --git a/.github/workflows/update-checksums-file.js b/.github/workflows/update-checksums-file.js new file mode 100644 index 0000000..22eecfd --- /dev/null +++ b/.github/workflows/update-checksums-file.js @@ -0,0 +1,94 @@ +/* + * Updates the `wrapper-checksums.json` file + * + * This is intended to be executed by the GitHub workflow, but can also be run + * manually. + */ + +// @ts-check + +const httpm = require('../../sources/node_modules/typed-rest-client/HttpClient') + +const path = require('path') +const fs = require('fs') + +/** + * @returns {Promise} + */ +async function main() { + const httpc = new httpm.HttpClient( + 'gradle/wrapper-validation-action/update-checksums-workflow', + undefined, + {allowRetries: true, maxRetries: 3} + ) + + /** + * @param {string} url + * @returns {Promise} + */ + async function httpGetText(url) { + const response = await httpc.get(url) + return await response.readBody() + } + + /** + * @typedef {Object} ApiVersionEntry + * @property {string} version - version name + * @property {string=} wrapperChecksumUrl - wrapper checksum URL; not present for old versions + * @property {boolean} snapshot - whether this is a snapshot version + */ + + /** + * @returns {Promise} + */ + async function httpGetVersions() { + return JSON.parse( + await httpGetText('https://services.gradle.org/versions/all') + ) + } + + const versions = (await httpGetVersions()) + // Only include versions with checksum + .filter(e => e.wrapperChecksumUrl !== undefined) + // Ignore snapshots; they are changing frequently so no point in including them in checksums file + .filter(e => !e.snapshot) + console.info(`Got ${versions.length} relevant Gradle versions`) + + // Note: For simplicity don't sort the entries but keep the order from the API; this also has the + // advantage that the latest versions come first, so compared to appending versions at the end + // this will not cause redundant Git diff due to trailing `,` being forbidden by JSON + + /** + * @typedef {Object} FileVersionEntry + * @property {string} version + * @property {string} checksum + */ + /** @type {FileVersionEntry[]} */ + const fileVersions = [] + for (const entry of versions) { + /** @type {string} */ + // @ts-ignore + const checksumUrl = entry.wrapperChecksumUrl + const checksum = await httpGetText(checksumUrl) + fileVersions.push({version: entry.version, checksum}) + } + + const jsonPath = path.resolve( + __dirname, + '..', + '..', + 'sources', + 'src', + 'wrapper-validation', + 'wrapper-checksums.json' + ) + console.info(`Writing checksums file to ${jsonPath}`) + // Write pretty-printed JSON (and add trailing line break) + fs.writeFileSync(jsonPath, JSON.stringify(fileVersions, null, 2) + '\n') +} + +main().catch(e => { + console.error(e) + // Manually set error exit code, otherwise error is logged but script exits successfully + process.exitCode = 1 +}) diff --git a/.github/workflows/update-checksums-file.yml b/.github/workflows/update-checksums-file.yml new file mode 100644 index 0000000..d7b4aaf --- /dev/null +++ b/.github/workflows/update-checksums-file.yml @@ -0,0 +1,55 @@ +name: 'Update Wrapper checksums file' + +on: + # Run weekly (at arbitrary time) + schedule: + - cron: '24 5 * * 6' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-checksums: + name: Update checksums + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: sources/package-lock.json + + - name: Install dependencies + run: | + npm install typed-rest-client@1.8.11 --no-save + working-directory: sources + + - name: Update checksums file + run: node ../.github/workflows/update-checksums-file.js + working-directory: sources + + # If there are no changes, this action will not create a pull request + - name: Create or update pull request + uses: peter-evans/create-pull-request@v6 + with: + branch: bot/wrapper-checksums-update + commit-message: Update known wrapper checksums + title: Update known wrapper checksums + # Note: Unfortunately this action cannot trigger the regular workflows for the PR automatically, see + # https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs + # Therefore suggest below to close and then reopen the PR + body: | + Automatically generated pull request to update the known wrapper checksums. + + In case of conflicts, manually run the workflow from the [Actions tab](https://github.com/gradle/wrapper-validation-action/actions/workflows/update-checksums-file.yml), the changes will then be force-pushed onto this pull request branch. + Do not manually update the pull request branch; those changes might get overwritten. + + > [!IMPORTANT] + > GitHub workflows have not been executed for this pull request yet. Before merging, close and then directly reopen this pull request to trigger the workflows. From fa4107aefb1394b373756c4b52cdd77749bde00c Mon Sep 17 00:00:00 2001 From: daz Date: Thu, 11 Apr 2024 09:04:13 -0600 Subject: [PATCH 3/4] Add docs for wrapper-validation action --- README.md | 23 ++++++++ docs/wrapper-validation.md | 106 +++++++++++++++++++++++++++++++++++ wrapper-validation/README.md | 22 ++++++++ 3 files changed, 151 insertions(+) create mode 100644 docs/wrapper-validation.md create mode 100644 wrapper-validation/README.md diff --git a/README.md b/README.md index fbf352d..2ad3ea6 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,26 @@ jobs: ``` See the [full action documentation](docs/dependency-submission.md) for more advanced usage scenarios. + +## The `wrapper-validation` action + +The `wrapper-validation` action validates the checksums of _all_ [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) JAR files present in the repository and fails if any unknown Gradle Wrapper JAR files are found. + +The action should be run in the root of the repository, as it will recursively search for any files named `gradle-wrapper.jar`. + +### Example workflow + +```yaml +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v3 +``` + +See the [full action documentation](docs/wrapper-validation.md) for more advanced usage scenarios. diff --git a/docs/wrapper-validation.md b/docs/wrapper-validation.md new file mode 100644 index 0000000..28ce668 --- /dev/null +++ b/docs/wrapper-validation.md @@ -0,0 +1,106 @@ +# Gradle Wrapper Validation Action + +This action validates the checksums of _all_ [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) JAR files present in the repository and fails if any unknown Gradle Wrapper JAR files are found. + +The action should be run in the root of the repository, as it will recursively search for any files named `gradle-wrapper.jar`. + +## The Gradle Wrapper Problem in Open Source + +The `gradle-wrapper.jar` is a binary blob of executable code that is checked into nearly +[2.8 Million GitHub Repositories](https://github.com/search?l=&q=filename%3Agradle-wrapper.jar&type=Code). + +Searching across GitHub you can find many pull requests (PRs) with helpful titles like 'Update to Gradle xxx'. +Many of these PRs are contributed by individuals outside of the organization maintaining the project. + +Many maintainers are incredibly grateful for these kinds of contributions as it takes an item off of their backlog. +We assume that most maintainers do not consider the security implications of accepting the Gradle Wrapper binary from external contributors. +There is a certain amount of blind trust open source maintainers have. +Further compounding the issue is that maintainers are most often greeted in these PRs with a diff to the `gradle-wrapper.jar` that looks like this. + +![Image of a GitHub Diff of Gradle Wrapper displaying text 'Binary file not shown.'](https://user-images.githubusercontent.com/1323708/71915219-477d7780-3149-11ea-9254-90c80dbffb0a.png) + +A fairly simple social engineering supply chain attack against open source would be contribute a helpful “Updated to Gradle xxx” PR that contains malicious code hidden inside this binary JAR. +A malicious `gradle-wrapper.jar` could execute, download, or install arbitrary code while otherwise behaving like a completely normal `gradle-wrapper.jar`. + +## Solution + +We have created a simple GitHub Action that can be applied to any GitHub repository. +This GitHub Action will do one simple task: +verify that any and all `gradle-wrapper.jar` files in the repository match the SHA-256 checksums of any of our official releases. + +If any are found that do not match the SHA-256 checksums of our official releases, the action will fail. + +Additionally, the action will find and SHA-256 hash all +[homoglyph](https://en.wikipedia.org/wiki/Homoglyph) +variants of files named `gradle-wrapper.jar`, +for example a file named `gradlе-wrapper.jar` (which uses a Cyrillic `е` instead of `e`). +The goal is to prevent homoglyph attacks which may be very difficult to spot in a GitHub diff. +We created an example [Homoglyph attack PR here](https://github.com/JLLeitschuh/playframework/pull/1/files). + +## Usage + +### Add to an existing Workflow + +Simply add this action to your workflow **after** having checked out your source tree and **before** running any Gradle build: + +```yaml +uses: gradle/actions/wrapper-validation@v3 +``` + +This action step should precede any step using `gradle/gradle-build-action` or `gradle/actions/setup-gradle`. + +### Add a new dedicated Workflow + +Here's a sample complete workflow you can add to your repositories: + +**`.github/workflows/gradle-wrapper-validation.yml`** +```yaml +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v3 +``` + +## Contributing to an external GitHub Repository + +Since [GitHub Actions](https://github.com/features/actions) +are completely free for open source projects and are automatically enabled on almost all projects, +adding this check to a project's build is as simple as contributing a PR. +Enabling the check requires no overhead on behalf of the project maintainer beyond merging the action. + +You can add this action to your favorite Gradle based project without checking out their source locally via the +GitHub Web UI thanks to the 'Create new file' button. + +![GitHub 'Create new file' Button bar picture](https://user-images.githubusercontent.com/1323708/73676469-6c023c00-4682-11ea-8c0a-5a1e2d29b17f.png) + +Simply add a new file named `.github/workflows/gradle-wrapper-validation.yml` with the contents mentioned above. + +We recommend the message commit contents of: + - Title: `Official Gradle Wrapper Validation Action` + - Body (at minimum): `See: https://github.com/gradle/actions/wrapper-validation` + +From there, you can easily follow the rest of the prompts to create a Pull Request against the project. + +## Reporting Failures + +If this GitHub action fails because a `gradle-wrapper.jar` doesn't match one of our published SHA-256 checksums, +we highly recommend that you reach out to us at [security@gradle.com](mailto:security@gradle.com). + +**Note:** `gradle-wrapper.jar` generated by Gradle 3.3 to 4.0 are not verifiable because those files were dynamically generated by Gradle in a non-reproducible way. It's not possible to verify the `gradle-wrapper.jar` for those versions are legitimate using a hash comparison. You should try to determine if the `gradle-wrapper.jar` was generated by one of these versions before running the build. + +If the Gradle version in `gradle-wrapper.properties` is out of this range, you may need to regenerate the `gradle-wrapper.jar` by running `./gradlew wrapper`. If you need to use a version of Gradle between 3.3 and 4.0, you can use a newer version of Gradle to generate the `gradle-wrapper.jar`. + +If you're curious and want to explore what the differences are between the `gradle-wrapper.jar` in your possession +and one of our valid release, you can compare them using this online utility: [diffoscope](https://try.diffoscope.org/). +Regardless of what you find, we still kindly request that you reach out to us and let us know. + +## Resources + +To learn more about verifying the Gradle Wrapper JAR locally, see our +[guide on the topic](https://docs.gradle.org/current/userguide/gradle_wrapper.html#wrapper_checksum_verification). diff --git a/wrapper-validation/README.md b/wrapper-validation/README.md new file mode 100644 index 0000000..e89b918 --- /dev/null +++ b/wrapper-validation/README.md @@ -0,0 +1,22 @@ +## The `wrapper-validation` action + +The `wrapper-validation` action validates the checksums of _all_ [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) JAR files present in the repository and fails if any unknown Gradle Wrapper JAR files are found. + +The action should be run in the root of the repository, as it will recursively search for any files named `gradle-wrapper.jar`. + +### Example workflow + +```yaml +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v3 +``` + +See the [full action documentation](../docs/wrapper-validation.md) for more advanced usage scenarios. From c1a61df4f10c508b181ba36f6d09b368d33fc896 Mon Sep 17 00:00:00 2001 From: daz Date: Thu, 11 Apr 2024 09:45:42 -0600 Subject: [PATCH 4/4] Remove unused dependency --- sources/package-lock.json | 1 - sources/package.json | 3 --- 2 files changed, 4 deletions(-) diff --git a/sources/package-lock.json b/sources/package-lock.json index f24ad98..a581233 100644 --- a/sources/package-lock.json +++ b/sources/package-lock.json @@ -35,7 +35,6 @@ "eslint-plugin-github": "4.10.2", "eslint-plugin-jest": "27.9.0", "eslint-plugin-prettier": "5.1.3", - "glob-parent": "6.0.2", "jest": "29.7.0", "js-yaml": "4.1.0", "nock": "13.5.4", diff --git a/sources/package.json b/sources/package.json index e5e4b70..f2653f2 100644 --- a/sources/package.json +++ b/sources/package.json @@ -44,7 +44,6 @@ "@octokit/webhooks-types": "7.5.0", "semver": "7.6.0", "string-argv": "0.3.2", - "typed-rest-client": "1.8.11", "unhomoglyph": "1.0.6" }, @@ -65,8 +64,6 @@ "prettier": "3.2.5", "ts-jest": "29.1.2", "typescript": "5.4.3", - - "glob-parent": "6.0.2", "nock": "13.5.4" } }