- 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
This commit is contained in:
Mike Penz 2024-02-23 16:51:17 +00:00 committed by GitHub
parent 63d15e7a1e
commit ba65536530
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 249 additions and 18 deletions

View file

@ -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()
})

View file

@ -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

View file

@ -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

View file

@ -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')
})

View file

@ -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)

View file

@ -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:

74
dist/index.js vendored
View file

@ -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) => {

View file

@ -33,7 +33,9 @@ function getKnownValidChecksums(): Map<string, Set<string>> {
export const KNOWN_VALID_CHECKSUMS = getKnownValidChecksums()
export async function fetchValidChecksums(
allowSnapshots: boolean
allowSnapshots: boolean,
detectVersions: boolean,
detectedVersions: string[]
): Promise<Set<string>> {
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

View file

@ -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<string[]> {
const files = await recursivelyListFiles(baseDir)
@ -13,6 +17,63 @@ export async function findWrapperJars(baseDir: string): Promise<string[]> {
.sort((a, b) => a.localeCompare(b))
}
export async function detectVersions(wrapperJars: string[]): Promise<string[]> {
return (
await Promise.all(
wrapperJars.map(async wrapperJar => await findWrapperVersion(wrapperJar))
)
).filter(version => version !== undefined) as string[]
}
async function findWrapperVersion(
wrapperJar: string
): Promise<string | undefined> {
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<string[]> {
const childrenNames = await readdir(baseDir)
const childrenPaths = await Promise.all(

View file

@ -9,7 +9,8 @@ export async function run(): Promise<void> {
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())

View file

@ -7,6 +7,7 @@ export async function findInvalidWrapperJars(
minWrapperCount: number,
allowSnapshots: boolean,
allowedChecksums: string[],
detectVersions: boolean,
knownValidChecksums: Map<
string,
Set<string>
@ -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)) {