From ba65536530caa264d8302f30a53fa1248a7a28f0 Mon Sep 17 00:00:00 2001 From: Mike Penz Date: Fri, 23 Feb 2024 16:51:17 +0000 Subject: [PATCH] - introduce new configuration to enable checksum validation only for detected versions - version is detected from gradle-wrapper.properties - checksum is only fetched for these particular versions - FIX https://github.com/gradle/wrapper-validation-action/issues/96 - update action.yml with new config option - update and introduce testcases for the new configuration option --- __tests__/checksums.test.ts | 15 +++- .../data/invalid/gradle-wrapper.properties | 7 ++ .../data/valid/gradle-wrapper.properties | 5 ++ __tests__/find.test.ts | 10 +++ __tests__/validate.test.ts | 65 ++++++++++++++-- action.yml | 4 + dist/index.js | 74 +++++++++++++++++-- src/checksums.ts | 8 +- src/find.ts | 61 +++++++++++++++ src/main.ts | 3 +- src/validate.ts | 15 +++- 11 files changed, 249 insertions(+), 18 deletions(-) create mode 100644 __tests__/data/invalid/gradle-wrapper.properties create mode 100644 __tests__/data/valid/gradle-wrapper.properties diff --git a/__tests__/checksums.test.ts b/__tests__/checksums.test.ts index 06b81d39..cee947cb 100644 --- a/__tests__/checksums.test.ts +++ b/__tests__/checksums.test.ts @@ -21,7 +21,7 @@ test('has loaded hardcoded wrapper jars checksums', async () => { }) test('fetches wrapper jars checksums', async () => { - const validChecksums = await checksums.fetchValidChecksums(false) + const validChecksums = await checksums.fetchValidChecksums(false, false, []) expect(validChecksums.size).toBeGreaterThan(10) // Verify that checksum of arbitrary version is contained expect( @@ -32,6 +32,13 @@ test('fetches wrapper jars checksums', async () => { ).toBe(true) }) +test('fetches wrapper jars checksums only for detected versions', async () => { + const validChecksums = await checksums.fetchValidChecksums(false, true, [ + '8.2.1' + ]) + expect(validChecksums.size).toBe(1) +}) + describe('retry', () => { afterEach(() => { nock.cleanAll() @@ -47,7 +54,11 @@ describe('retry', () => { code: 'ECONNREFUSED' }) - const validChecksums = await checksums.fetchValidChecksums(false) + const validChecksums = await checksums.fetchValidChecksums( + false, + false, + [] + ) expect(validChecksums.size).toBeGreaterThan(10) nock.isDone() }) diff --git a/__tests__/data/invalid/gradle-wrapper.properties b/__tests__/data/invalid/gradle-wrapper.properties new file mode 100644 index 00000000..78e42ce0 --- /dev/null +++ b/__tests__/data/invalid/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle8.2.1bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/__tests__/data/valid/gradle-wrapper.properties b/__tests__/data/valid/gradle-wrapper.properties new file mode 100644 index 00000000..2d79b772 --- /dev/null +++ b/__tests__/data/valid/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-milestone-3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true \ No newline at end of file diff --git a/__tests__/find.test.ts b/__tests__/find.test.ts index 2667fed1..27bf7afd 100644 --- a/__tests__/find.test.ts +++ b/__tests__/find.test.ts @@ -10,3 +10,13 @@ test('finds test data wrapper jars', async () => { expect(wrapperJars).toContain('__tests__/data/invalid/gradle-wrapper.jar') expect(wrapperJars).toContain('__tests__/data/invalid/gradlе-wrapper.jar') // homoglyph }) + +test('detect version from `gradle-wrapper.properties` alongside wrappers', async () => { + const repoRoot = path.resolve('.') + const wrapperJars = await find.findWrapperJars(repoRoot) + + const detectedVersions = await find.detectVersions(wrapperJars) + + expect(detectedVersions.length).toBe(1) + expect(detectedVersions).toContain('6.1-milestone-3') +}) diff --git a/__tests__/validate.test.ts b/__tests__/validate.test.ts index 6abf48b2..3542c08c 100644 --- a/__tests__/validate.test.ts +++ b/__tests__/validate.test.ts @@ -7,9 +7,13 @@ jest.setTimeout(30000) const baseDir = path.resolve('.') test('succeeds if all found wrapper jars are valid', async () => { - const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [ - 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - ]) + const result = await validate.findInvalidWrapperJars( + baseDir, + 3, + false, + ['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'], + false + ) expect(result.isValid()).toBe(true) // Only hardcoded and explicitly allowed checksums should have been used @@ -30,6 +34,7 @@ test('succeeds if all found wrapper jars are valid (and checksums are fetched fr 1, false, ['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'], + false, knownValidChecksums ) @@ -46,7 +51,51 @@ test('succeeds if all found wrapper jars are valid (and checksums are fetched fr }) test('fails if invalid wrapper jars are found', async () => { - const result = await validate.findInvalidWrapperJars(baseDir, 3, false, []) + const result = await validate.findInvalidWrapperJars( + baseDir, + 3, + false, + [], + false + ) + + expect(result.isValid()).toBe(false) + + expect(result.valid).toEqual([ + new validate.WrapperJar( + '__tests__/data/valid/gradle-wrapper.jar', + '3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce' + ) + ]) + + expect(result.invalid).toEqual([ + new validate.WrapperJar( + '__tests__/data/invalid/gradle-wrapper.jar', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ), + new validate.WrapperJar( + '__tests__/data/invalid/gradlе-wrapper.jar', // homoglyph + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ) + ]) + + expect(result.toDisplayString()).toBe( + '✗ Found unknown Gradle Wrapper JAR files:\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 __tests__/data/invalid/gradle-wrapper.jar\n' + + ' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 __tests__/data/invalid/gradlе-wrapper.jar\n' + // homoglyph + '✓ Found known Gradle Wrapper JAR files:\n' + + ' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce __tests__/data/valid/gradle-wrapper.jar' + ) +}) + +test('fails if invalid wrapper jars are found when detection versions from `gradle-wrapper.properties`', async () => { + const result = await validate.findInvalidWrapperJars( + baseDir, + 3, + false, + [], + true + ) expect(result.isValid()).toBe(false) @@ -78,7 +127,13 @@ test('fails if invalid wrapper jars are found', async () => { }) test('fails if not enough wrapper jars are found', async () => { - const result = await validate.findInvalidWrapperJars(baseDir, 4, false, []) + const result = await validate.findInvalidWrapperJars( + baseDir, + 4, + false, + [], + false + ) expect(result.isValid()).toBe(false) diff --git a/action.yml b/action.yml index 192b54ba..0124b452 100644 --- a/action.yml +++ b/action.yml @@ -15,6 +15,10 @@ inputs: description: 'Accept arbitrary user-defined checksums as valid. Comma separated list of SHA256 checksums (lowercase hex).' required: false default: '' + detect-version: + description: 'Searches for the Gradle version defined in the gradle-wrapper.properties file to limit checksum verification to these versions. Boolean, true or false.' + required: false + default: 'false' outputs: failed-wrapper: diff --git a/dist/index.js b/dist/index.js index 03c3c05a..c946d1dd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -27865,14 +27865,15 @@ function getKnownValidChecksums() { * Maps from the checksum to the names of the Gradle versions whose wrapper has this checksum. */ exports.KNOWN_VALID_CHECKSUMS = getKnownValidChecksums(); -async function fetchValidChecksums(allowSnapshots) { +async function fetchValidChecksums(allowSnapshots, detectVersions, detectedVersions) { 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) => allowSnapshots || !entry.snapshot); + (entry) => (allowSnapshots || !entry.snapshot) && + (!detectVersions || detectedVersions.includes(entry.version))); const checksumUrls = allowed.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (entry) => entry.wrapperChecksumUrl); @@ -27923,12 +27924,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.findWrapperJars = void 0; +exports.detectVersions = exports.findWrapperJars = void 0; const util = __importStar(__nccwpck_require__(3837)); const path = __importStar(__nccwpck_require__(1017)); const fs = __importStar(__nccwpck_require__(7147)); +const readline = __importStar(__nccwpck_require__(4521)); const unhomoglyph_1 = __importDefault(__nccwpck_require__(8708)); +const core = __importStar(__nccwpck_require__(2186)); +const events_1 = __importDefault(__nccwpck_require__(2361)); const readdir = util.promisify(fs.readdir); +const versionRegex = new RegExp(/\/gradle-(.+)-/); async function findWrapperJars(baseDir) { const files = await recursivelyListFiles(baseDir); return files @@ -27937,6 +27942,48 @@ async function findWrapperJars(baseDir) { .sort((a, b) => a.localeCompare(b)); } exports.findWrapperJars = findWrapperJars; +async function detectVersions(wrapperJars) { + return (await Promise.all(wrapperJars.map(async (wrapperJar) => await findWrapperVersion(wrapperJar)))).filter(version => version !== undefined); +} +exports.detectVersions = detectVersions; +async function findWrapperVersion(wrapperJar) { + const jar = path.parse(wrapperJar); + const properties = path.resolve(jar.dir, 'gradle-wrapper.properties'); + if (fs.existsSync(properties)) { + try { + const lineReader = readline.createInterface({ + input: fs.createReadStream(properties) + }); + let distributionUrl = ''; + lineReader.on('line', function (line) { + if (line.startsWith('distributionUrl=')) { + distributionUrl = line; + lineReader.close(); + } + }); + await events_1.default.once(lineReader, 'close'); + if (distributionUrl) { + const matchedVersion = distributionUrl.match(versionRegex); + if (matchedVersion && matchedVersion.length >= 1) { + return matchedVersion[1]; + } + else { + core.debug(`Could not parse version from distributionUrl in gradle-wrapper.properties file: ${properties}`); + } + } + else { + core.debug(`Could not identify valid distributionUrl in gradle-wrapper.properties file: ${properties}`); + } + } + catch (error) { + core.warning(`Failed to retrieve version from gradle-wrapper.properties file: ${properties} due to ${error}`); + } + } + else { + core.debug(`No gradle-wrapper.properties file existed alongside ${wrapperJar}`); + } + return undefined; +} async function recursivelyListFiles(baseDir) { const childrenNames = await readdir(baseDir); const childrenPaths = await Promise.all(childrenNames.map(async (childName) => { @@ -28038,7 +28085,7 @@ const core = __importStar(__nccwpck_require__(2186)); const validate = __importStar(__nccwpck_require__(4953)); async function run() { try { - const result = await validate.findInvalidWrapperJars(path.resolve('.'), +core.getInput('min-wrapper-count'), core.getInput('allow-snapshots') === 'true', core.getInput('allow-checksums').split(',')); + const result = await validate.findInvalidWrapperJars(path.resolve('.'), +core.getInput('min-wrapper-count'), core.getInput('allow-snapshots') === 'true', core.getInput('allow-checksums').split(','), core.getInput('detect-version') === 'true'); if (result.isValid()) { core.info(result.toDisplayString()); } @@ -28103,7 +28150,7 @@ exports.WrapperJar = exports.ValidationResult = exports.findInvalidWrapperJars = const find = __importStar(__nccwpck_require__(3288)); const checksums = __importStar(__nccwpck_require__(1541)); const hash = __importStar(__nccwpck_require__(9778)); -async function findInvalidWrapperJars(gitRepoRoot, minWrapperCount, allowSnapshots, allowedChecksums, knownValidChecksums = checksums.KNOWN_VALID_CHECKSUMS) { +async function findInvalidWrapperJars(gitRepoRoot, minWrapperCount, allowSnapshots, allowedChecksums, detectVersions, knownValidChecksums = checksums.KNOWN_VALID_CHECKSUMS) { const wrapperJars = await find.findWrapperJars(gitRepoRoot); const result = new ValidationResult([], []); if (wrapperJars.length < minWrapperCount) { @@ -28123,7 +28170,14 @@ async function findInvalidWrapperJars(gitRepoRoot, minWrapperCount, allowSnapsho // 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); + let detectedVersions; + if (detectVersions) { + detectedVersions = await find.detectVersions(wrapperJars); + } + else { + detectedVersions = []; + } + const fetchedValidChecksums = await checksums.fetchValidChecksums(allowSnapshots, detectVersions, detectedVersions); for (const wrapperJar of notYetValidatedWrappers) { if (!fetchedValidChecksums.has(wrapperJar.checksum)) { result.invalid.push(wrapperJar); @@ -28335,6 +28389,14 @@ module.exports = require("querystring"); /***/ }), +/***/ 4521: +/***/ ((module) => { + +"use strict"; +module.exports = require("readline"); + +/***/ }), + /***/ 2781: /***/ ((module) => { diff --git a/src/checksums.ts b/src/checksums.ts index 21658346..c76ed3d7 100644 --- a/src/checksums.ts +++ b/src/checksums.ts @@ -33,7 +33,9 @@ function getKnownValidChecksums(): Map> { export const KNOWN_VALID_CHECKSUMS = getKnownValidChecksums() export async function fetchValidChecksums( - allowSnapshots: boolean + allowSnapshots: boolean, + detectVersions: boolean, + detectedVersions: string[] ): Promise> { const all = await httpGetJsonArray('https://services.gradle.org/versions/all') const withChecksum = all.filter( @@ -44,7 +46,9 @@ export async function fetchValidChecksums( ) const allowed = withChecksum.filter( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (entry: any) => allowSnapshots || !entry.snapshot + (entry: any) => + (allowSnapshots || !entry.snapshot) && + (!detectVersions || detectedVersions.includes(entry.version)) ) const checksumUrls = allowed.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/find.ts b/src/find.ts index 66f14b9e..71f8abc4 100644 --- a/src/find.ts +++ b/src/find.ts @@ -1,9 +1,13 @@ import * as util from 'util' import * as path from 'path' import * as fs from 'fs' +import * as readline from 'readline' import unhomoglyph from 'unhomoglyph' +import * as core from '@actions/core' +import events from 'events' const readdir = util.promisify(fs.readdir) +const versionRegex = new RegExp(/\/gradle-(.+)-/) export async function findWrapperJars(baseDir: string): Promise { const files = await recursivelyListFiles(baseDir) @@ -13,6 +17,63 @@ export async function findWrapperJars(baseDir: string): Promise { .sort((a, b) => a.localeCompare(b)) } +export async function detectVersions(wrapperJars: string[]): Promise { + return ( + await Promise.all( + wrapperJars.map(async wrapperJar => await findWrapperVersion(wrapperJar)) + ) + ).filter(version => version !== undefined) as string[] +} + +async function findWrapperVersion( + wrapperJar: string +): Promise { + const jar = path.parse(wrapperJar) + const properties = path.resolve(jar.dir, 'gradle-wrapper.properties') + + if (fs.existsSync(properties)) { + try { + const lineReader = readline.createInterface({ + input: fs.createReadStream(properties) + }) + + let distributionUrl = '' + lineReader.on('line', function (line) { + if (line.startsWith('distributionUrl=')) { + distributionUrl = line + lineReader.close() + } + }) + + await events.once(lineReader, 'close') + + if (distributionUrl) { + const matchedVersion = distributionUrl.match(versionRegex) + if (matchedVersion && matchedVersion.length >= 1) { + return matchedVersion[1] + } else { + core.debug( + `Could not parse version from distributionUrl in gradle-wrapper.properties file: ${properties}` + ) + } + } else { + core.debug( + `Could not identify valid distributionUrl in gradle-wrapper.properties file: ${properties}` + ) + } + } catch (error) { + core.warning( + `Failed to retrieve version from gradle-wrapper.properties file: ${properties} due to ${error}` + ) + } + } else { + core.debug( + `No gradle-wrapper.properties file existed alongside ${wrapperJar}` + ) + } + return undefined +} + async function recursivelyListFiles(baseDir: string): Promise { const childrenNames = await readdir(baseDir) const childrenPaths = await Promise.all( diff --git a/src/main.ts b/src/main.ts index 041a3d4e..2ddad211 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,8 @@ export async function run(): Promise { path.resolve('.'), +core.getInput('min-wrapper-count'), core.getInput('allow-snapshots') === 'true', - core.getInput('allow-checksums').split(',') + core.getInput('allow-checksums').split(','), + core.getInput('detect-version') === 'true' ) if (result.isValid()) { core.info(result.toDisplayString()) diff --git a/src/validate.ts b/src/validate.ts index 95ae2fdf..960d9ae4 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -7,6 +7,7 @@ export async function findInvalidWrapperJars( minWrapperCount: number, allowSnapshots: boolean, allowedChecksums: string[], + detectVersions: boolean, knownValidChecksums: Map< string, Set @@ -33,8 +34,18 @@ export async function findInvalidWrapperJars( // 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) + + let detectedVersions: string[] + if (detectVersions) { + detectedVersions = await find.detectVersions(wrapperJars) + } else { + detectedVersions = [] + } + const fetchedValidChecksums = await checksums.fetchValidChecksums( + allowSnapshots, + detectVersions, + detectedVersions + ) for (const wrapperJar of notYetValidatedWrappers) { if (!fetchedValidChecksums.has(wrapperJar.checksum)) {