Delegate to 'gradle/actions/wrapper-validation' (#200)

Now that 'gradle/actions/wrapper-validation' has been released as v3.3.0, 
we remove this implementation and delegate via a composite action.

Fixes #198
This commit is contained in:
Daz DeBoer 2024-04-12 15:03:36 -06:00 committed by GitHub
parent b5418f5a58
commit 460a3ca55f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 32 additions and 39315 deletions

View file

@ -1,3 +0,0 @@
dist/
lib/
node_modules/

View file

@ -1,54 +0,0 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"i18n-text/no-en": "off",
"no-unused-vars": "off",
"sort-imports": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"camelcase": "off",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

View file

@ -8,14 +8,3 @@ updates:
github-actions: github-actions:
patterns: patterns:
- "*" - "*"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "@types/node"
groups:
npm-dependencies:
patterns:
- "*"

View file

@ -1,51 +0,0 @@
# `dist/index.js` is a special file in Actions.
# When you reference an action with `uses:` in a workflow,
# `index.js` is the code that will run.
# For our project, we generate this file through a build process from other source files.
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
name: Check dist directory
on:
push:
branches:
- main
- releases/**
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build
run: |
npm -v
node -v
npm clean-install
npm run build
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
id: diff
# If index.js was different than expected, upload the expected version as an artifact
- uses: actions/upload-artifact@v4
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: dist/

View file

@ -7,22 +7,6 @@ on: # rebuild any PRs and main branch changes
- 'releases/*' - 'releases/*'
jobs: jobs:
build: # make sure build/ci work properly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Build and test
run: |
npm -v
node -v
npm clean-install
npm run all
# Integration test for successful validation of wrappers # Integration test for successful validation of wrappers
test-validation-success: test-validation-success:
@ -31,19 +15,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build action (pull request)
# Pull requests are not expected to update `dist/index.js` themselves; therefore build `dist/index.js`
# here before running integration test
if: github.event_name == 'pull_request'
run: |
npm clean-install
npm run build
- name: Run wrapper-validation-action - name: Run wrapper-validation-action
id: action-test id: action-test
uses: ./ uses: ./
@ -71,19 +42,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build action (pull request)
# Pull requests are not expected to update `dist/index.js` themselves; therefore build `dist/index.js`
# here before running integration test
if: github.event_name == 'pull_request'
run: |
npm clean-install
npm run build
- name: Run wrapper-validation-action - name: Run wrapper-validation-action
id: action-test id: action-test
uses: ./ uses: ./

View file

@ -1,56 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '24 4 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View file

@ -1,92 +0,0 @@
/*
* 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('typed-rest-client/HttpClient')
const path = require('path')
const fs = require('fs')
/**
* @returns {Promise<void>}
*/
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<string>}
*/
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<ApiVersionEntry[]>}
*/
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,
'..',
'..',
'src',
'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
})

View file

@ -1,49 +0,0 @@
name: 'Update Wrapper checksums file'
on:
schedule:
# Run weekly (at arbitrary time)
- cron: '24 5 * * 6'
# Support running workflow manually
workflow_dispatch:
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
- name: Install dependencies
run: |
npm install typed-rest-client@1.8.11 --no-save
- name: Update checksums file
run: node ./.github/workflows/update-checksums-file.js
# 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.

102
.gitignore vendored
View file

@ -1,102 +0,0 @@
# Dependency directory
node_modules
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
__tests__/runner/*
lib/**/*
.idea/
*.iml

View file

@ -1,3 +0,0 @@
dist/
lib/
node_modules/

View file

@ -1,12 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript",
"endOfLine": "auto"
}

View file

@ -1,2 +0,0 @@
# Configuration file for asdf version manager
nodejs 20.10.0

View file

@ -1,12 +0,0 @@
## Project Goals
We aim to keep the scope of this project limited so that it is easy for maintainers to apply and forget about.
### Goals
To verify that all the gradle-wrapper.jar(s) in a given GitHub repository or pull request against that repo is an official Gradle Wrapper release.
### Non-Goals
It is not the goal of this action to verify that the gradle-wrapper.jar matches a specific version of Gradle,
nor that the version declared in the build.gradle or gradle-wrapper.properties file matches.

109
README.md
View file

@ -1,6 +1,18 @@
<p align="center"> > [!IMPORTANT]
<a href="https://github.com/gradle/wrapper-validation-action/actions"><img alt="gradle/wrapper-validation-action status" src="https://github.com/gradle/wrapper-validation-action/workflows/ci/badge.svg"></a> > As of `v3` this action has been superceded by `gradle/actions/wrapper-validation`.
</p> > Any workflow that uses `gradle/wrapper-validation-action@v3` will transparently delegate to `gradle/actions/wrapper-validation@v3`.
>
> Users are encouraged to update their workflows, replacing:
> ```
> uses: gradle/wrapper-validation-action@v3
> ```
>
> with
> ```
> uses: gradle/actions/wrapper-validation@v3
> ```
>
> See the [wrapper-validation documentation](https://github.com/gradle/actions/tree/main/wrapper-validation) for up-to-date documentation for `gradle/actions/wrapper-validation`.
# Gradle Wrapper Validation Action # Gradle Wrapper Validation Action
@ -8,56 +20,8 @@ This action validates the checksums of _all_ [Gradle Wrapper](https://docs.gradl
The action should be run in the root of the repository, as it will recursively search for any files named `gradle-wrapper.jar`. 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 ### Example workflow
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/wrapper-validation-action@v2
```
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 ```yaml
name: "Validate Gradle Wrapper" name: "Validate Gradle Wrapper"
on: [push, pull_request] on: [push, pull_request]
@ -68,43 +32,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2 - uses: gradle/wrapper-validation-action@v3
``` ```
## Contributing to an external GitHub Repository As of `v3`, the `gradle/wrapper-validation-action` action delegates to `gradle/actions/wrapper-validation` with the same version.
Configuration and usage of these actions is identical for releases with the same version number.
Since [GitHub Actions](https://github.com/features/actions) See the [full wrapper-validation documentation](https://github.com/gradle/actions/tree/main/wrapper-validation) for more details.
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/wrapper-validation-action`
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).

View file

@ -1,14 +0,0 @@
# Release
* starting on `main`
* `npm install`
* `npm run all`
* Commit and push any changes to the `dist` directory. Wait for CI.
* `git tag v1.0.x && git push --tags` with the actual version number
* `git tag -f -a v1 && git push -f --tags`
* go to https://github.com/gradle/wrapper-validation-action/releases
* edit and publish the now drafted `v1` release
* create a new release from the new full version number `v1.0.x`, list the fixed issues and publish the release
* go to https://github.com/marketplace/actions/gradle-wrapper-validation
* verify that it displays the latest README
* verify that the version dropdown displays the new version

View file

@ -1,55 +0,0 @@
import * as checksums from '../src/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()
})
})
})

View file

@ -1,12 +0,0 @@
import * as path from 'path'
import * as find from '../src/find'
import {expect, test} from '@jest/globals'
test('finds test data wrapper jars', async () => {
const repoRoot = path.resolve('.')
const wrapperJars = await find.findWrapperJars(repoRoot)
expect(wrapperJars.length).toBe(3)
expect(wrapperJars).toContain('__tests__/data/valid/gradle-wrapper.jar')
expect(wrapperJars).toContain('__tests__/data/invalid/gradle-wrapper.jar')
expect(wrapperJars).toContain('__tests__/data/invalid/gradlе-wrapper.jar') // homoglyph
})

View file

@ -1,12 +0,0 @@
import * as path from 'path'
import * as hash from '../src/hash'
import {expect, test} from '@jest/globals'
test('can sha256 files', async () => {
const sha = await hash.sha256File(
path.resolve('__tests__/data/invalid/gradle-wrapper.jar')
)
expect(sha).toEqual(
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
)
})

View file

@ -1,98 +0,0 @@
import * as path from 'path'
import * as validate from '../src/validate'
import {expect, test, jest} from '@jest/globals'
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'
])
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 __tests__/data/invalid/gradle-wrapper.jar\n' +
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 __tests__/data/invalid/gradlе-wrapper.jar\n' + // homoglyph
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce __tests__/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<string, Set<string>>()
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 __tests__/data/invalid/gradle-wrapper.jar\n' +
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 __tests__/data/invalid/gradlе-wrapper.jar\n' + // homoglyph
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce __tests__/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(
'__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 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 __tests__/data/invalid/gradle-wrapper.jar\n' +
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 __tests__/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 __tests__/data/valid/gradle-wrapper.jar'
)
})

View file

@ -19,10 +19,20 @@ inputs:
outputs: outputs:
failed-wrapper: 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.' 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.'
value: ${{ steps.wrapper-validation.outputs.failed-wrapper }}
runs: runs:
using: 'node20' using: "composite"
main: 'dist/index.js' steps:
- name: Wrapper Validation
id: wrapper-validation
uses: gradle/actions/wrapper-validation@v3.3.0
with:
min-wrapper-count: ${{ inputs.min-wrapper-count }}
allow-snapshots: ${{ inputs.allow-snapshots }}
allow-checksums: ${{ inputs.allow-checksums }}
env:
GRADLE_ACTION_ID: gradle/wrapper-validation-action
branding: branding:
icon: 'shield' icon: 'shield'

30309
dist/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts', 'json'],
testMatch: ['**/*.test.ts'],
preset: 'ts-jest',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js']
}

View file

@ -1 +0,0 @@
jest.setTimeout(10000) // in milliseconds

6893
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,49 +0,0 @@
{
"name": "wrapper-validation-action",
"version": "0.0.0",
"private": true,
"description": "Gradle Wrapper Validation Action",
"main": "src/main.ts",
"scripts": {
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
"check": "npm run format && npm run lint",
"compile": "ncc build",
"test": "jest",
"build": "npm run check && npm run compile",
"all": "npm run build && npm test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/gradle/wrapper-validation-action.git"
},
"keywords": [
"actions",
"node",
"setup"
],
"author": "Gradle Inc.",
"license": "MIT",
"dependencies": {
"@actions/core": "1.10.1",
"typed-rest-client": "1.8.11",
"unhomoglyph": "1.0.6"
},
"devDependencies": {
"@types/node": "16.18.38",
"@typescript-eslint/parser": "7.4.0",
"@vercel/ncc": "0.38.1",
"eslint": "8.57.0",
"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",
"prettier": "3.2.5",
"ts-jest": "29.1.2",
"typescript": "5.4.3"
}
}

View file

@ -1,66 +0,0 @@
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<string, Set<string>> {
const versionsMap = new Map<string, Set<string>>()
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<Set<string>> {
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<unknown[]> {
return JSON.parse(await httpGetText(url))
}
async function httpGetText(url: string): Promise<string> {
const response = await httpc.get(url)
return await response.readBody()
}

View file

@ -1,27 +0,0 @@
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<string[]> {
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<string[]> {
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)
}

View file

@ -1,18 +0,0 @@
import * as crypto from 'crypto'
import * as fs from 'fs'
export async function sha256File(path: string): Promise<string> {
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)
})
})
}

View file

@ -1,41 +0,0 @@
import * as path from 'path'
import * as core from '@actions/core'
import * as validate from './validate'
export async function run(): Promise<void> {
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()

View file

@ -1,105 +0,0 @@
import * as find from './find'
import * as checksums from './checksums'
import * as hash from './hash'
export async function findInvalidWrapperJars(
gitRepoRoot: string,
minWrapperCount: number,
allowSnapshots: boolean,
allowedChecksums: string[],
knownValidChecksums: Map<
string,
Set<string>
> = checksums.KNOWN_VALID_CHECKSUMS
): Promise<ValidationResult> {
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(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}`
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +0,0 @@
{
"compilerOptions": {
"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'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"resolveJsonModule": true, /* Enable importing JSON files as module; used for importing wrapper checksums JSON */
},
"exclude": ["node_modules", "**/*.test.ts"]
}