Skip to content

Commit

Permalink
Add checksums to enforce immutability (#250)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Drucker <bvdrucker@gmail.com>
  • Loading branch information
avri-schneider and bendrucker authored Nov 7, 2024
1 parent cf531b8 commit 8093687
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 13 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ jobs:
if: matrix.tflint_version == 'latest'
run: tflint -v

integration-checksum:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Action
uses: ./
with:
tflint_version: 'v0.53.0'
# https://github.com/terraform-linters/tflint/releases/tag/v0.53.0
# checksums.txt
# tflint_linux_amd64
checksums: bb0a3a6043ea1bcd221fc95d49bac831bb511eb31946ca6a4050983e9e584578
- run: tflint -v

integration-matchers:
name: 'Integration test (tflint_version: ${{ matrix.tflint_version }})'
runs-on: ubuntu-latest
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ If version is `"latest"`, the action will get the latest version number using [O

Default: `"latest"`

### `checksums`

**Optional** A newline-delimited list of valid checksums (SHA256 hashes) for the downloaded TFLint binary. When set, the action will verify that the binary matches one of these checksums before proceeding.

This ensures that the downloaded binary for a given version is a known build. If your job runs in multiple operating systems or architectures, include appropriate checksums for all of them.

**Note:** Checksums ensure _immutability_, but do not verify integrity. To prove that checksums come from a known build in TFLint's official repository, use [GitHub’s Artifact Attestations](https://github.com/terraform-linters/tflint?tab=readme-ov-file#github-cli-recommended) or [cosign](https://github.com/terraform-linters/tflint?tab=readme-ov-file#cosign).


### `github_token`

Used to authenticate requests to the GitHub API to obtain release data from the TFLint repository. Authenticating will increase the [API rate limit](https://developer.github.com/v3/#rate-limiting). Any valid token is supported. No permissions are required.
Expand All @@ -32,7 +41,7 @@ Default: `"false"`
The following outputs are available when the `tflint_wrapper` input is enabled:

- `stdout` - The output (stdout) produced by the tflint command.
- `stderr` - The error output (stdout) produced by the tflint command.
- `stderr` - The error output (stderr) produced by the tflint command.
- `exitcode` - The exit code produced by the tflint command.

## Usage
Expand Down Expand Up @@ -66,7 +75,6 @@ jobs:
name: Setup TFLint
with:
tflint_version: v0.52.0

- name: Show version
run: tflint --version

Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ inputs:
description: Installs a wrapper script to wrap subsequent calls to `tflint` and expose `stdout`, `stderr`, and `exitcode` outputs
default: 'false'
required: false
checksums:
description: Newline-delimited list of valid checksums (SHA256 hashes) for the downloaded TFLint binary. When set, the action will verify that the binary matches one of these checksums before proceeding.
required: false
outputs:
stdout:
description: The output (stdout) produced by the tflint command. Only available if `tflint_wrapper` is set to `true`.
Expand Down
35 changes: 33 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9989,6 +9989,9 @@ function wrappy (fn, cb) {

const os = __nccwpck_require__(2037);
const path = __nccwpck_require__(1017);
const crypto = __nccwpck_require__(6113);
const fs = __nccwpck_require__(7147);
const { pipeline } = __nccwpck_require__(4845)

const core = __nccwpck_require__(2186);
const io = __nccwpck_require__(7436);
Expand Down Expand Up @@ -10041,10 +10044,29 @@ async function getTFLintVersion(inputVersion) {
return inputVersion;
}

async function downloadCLI(url) {
async function fileSHA256(filePath) {
const hash = crypto.createHash('sha256');
const fileStream = fs.createReadStream(filePath);

await pipeline(fileStream, hash);
return hash.digest('hex');
}

async function downloadCLI(url, checksums) {
core.debug(`Downloading tflint CLI from ${url}`);
const pathToCLIZip = await tc.downloadTool(url);

if (checksums.length > 0) {
core.debug('Verifying checksum of downloaded file');

const checksum = await fileSHA256(pathToCLIZip);

if (!checksums.includes(checksum)) {
throw new Error(`Mismatched checksum: expected one of ${checksums.join(', ')}, but got ${checksum}`);
}
core.debug('SHA256 hash verified successfully');
}

core.debug('Extracting tflint CLI zip file');
const pathToCLI = await tc.extractZip(pathToCLIZip);
core.debug(`tflint CLI path is ${pathToCLI}.`);
Expand Down Expand Up @@ -10089,6 +10111,7 @@ async function installWrapper(pathToCLI) {
async function run() {
try {
const inputVersion = core.getInput('tflint_version');
const checksums = core.getMultilineInput('checksums');
const wrapper = core.getInput('tflint_wrapper') === 'true';
const version = await getTFLintVersion(inputVersion);
const platform = mapOS(os.platform());
Expand All @@ -10097,7 +10120,7 @@ async function run() {
core.debug(`Getting download URL for tflint version ${version}: ${platform} ${arch}`);
const url = `https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${platform}_${arch}.zip`;

const pathToCLI = await downloadCLI(url);
const pathToCLI = await downloadCLI(url, checksums);

if (wrapper) {
await installWrapper(pathToCLI);
Expand Down Expand Up @@ -10208,6 +10231,14 @@ module.exports = require("stream");

/***/ }),

/***/ 4845:
/***/ ((module) => {

"use strict";
module.exports = require("stream/promises");

/***/ }),

/***/ 1576:
/***/ ((module) => {

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "setup-tflint-action",
"version": "2.0.0",
"description": "Install and setup TFLint executable Action",
"version": "2.1.0",
"description": "Install and setup TFLint executable Action with SHA256 verification",
"main": "index.js",
"scripts": {
"lint": "eslint --fix . src test",
Expand All @@ -24,7 +24,9 @@
"GitHub",
"Actions",
"TFLint",
"Terraform"
"Terraform",
"SHA256",
"Verification"
],
"license": "MIT",
"bugs": {
Expand Down
27 changes: 25 additions & 2 deletions src/setup-tflint.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const { pipeline } = require('stream/promises')

const core = require('@actions/core');
const io = require('@actions/io');
Expand Down Expand Up @@ -52,10 +55,29 @@ async function getTFLintVersion(inputVersion) {
return inputVersion;
}

async function downloadCLI(url) {
async function fileSHA256(filePath) {
const hash = crypto.createHash('sha256');
const fileStream = fs.createReadStream(filePath);

await pipeline(fileStream, hash);
return hash.digest('hex');
}

async function downloadCLI(url, checksums) {
core.debug(`Downloading tflint CLI from ${url}`);
const pathToCLIZip = await tc.downloadTool(url);

if (checksums.length > 0) {
core.debug('Verifying checksum of downloaded file');

const checksum = await fileSHA256(pathToCLIZip);

if (!checksums.includes(checksum)) {
throw new Error(`Mismatched checksum: expected one of ${checksums.join(', ')}, but got ${checksum}`);
}
core.debug('SHA256 hash verified successfully');
}

core.debug('Extracting tflint CLI zip file');
const pathToCLI = await tc.extractZip(pathToCLIZip);
core.debug(`tflint CLI path is ${pathToCLI}.`);
Expand Down Expand Up @@ -100,6 +122,7 @@ async function installWrapper(pathToCLI) {
async function run() {
try {
const inputVersion = core.getInput('tflint_version');
const checksums = core.getMultilineInput('checksums');
const wrapper = core.getInput('tflint_wrapper') === 'true';
const version = await getTFLintVersion(inputVersion);
const platform = mapOS(os.platform());
Expand All @@ -108,7 +131,7 @@ async function run() {
core.debug(`Getting download URL for tflint version ${version}: ${platform} ${arch}`);
const url = `https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${platform}_${arch}.zip`;

const pathToCLI = await downloadCLI(url);
const pathToCLI = await downloadCLI(url, checksums);

if (wrapper) {
await installWrapper(pathToCLI);
Expand Down
3 changes: 1 addition & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const tc = require('@actions/tool-cache');

const setup = require('../src/setup-tflint');

jest.mock('@actions/core');
jest.mock('@actions/tool-cache');
fs.chmodSync = jest.fn();

Expand All @@ -30,8 +29,8 @@ describe('Mock tests', () => {
});

test('add path should be called', async () => {
jest.spyOn(core, 'addPath');
await setup();

expect(core.addPath).toBeCalledTimes(1);
});
});

0 comments on commit 8093687

Please sign in to comment.