Skip to content

Commit 8093687

Browse files
Add checksums to enforce immutability (#250)
Co-authored-by: Ben Drucker <bvdrucker@gmail.com>
1 parent cf531b8 commit 8093687

File tree

8 files changed

+94
-13
lines changed

8 files changed

+94
-13
lines changed

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ jobs:
8888
if: matrix.tflint_version == 'latest'
8989
run: tflint -v
9090

91+
integration-checksum:
92+
runs-on: ubuntu-latest
93+
94+
steps:
95+
- uses: actions/checkout@v4
96+
- name: Use Action
97+
uses: ./
98+
with:
99+
tflint_version: 'v0.53.0'
100+
# https://github.com/terraform-linters/tflint/releases/tag/v0.53.0
101+
# checksums.txt
102+
# tflint_linux_amd64
103+
checksums: bb0a3a6043ea1bcd221fc95d49bac831bb511eb31946ca6a4050983e9e584578
104+
- run: tflint -v
105+
91106
integration-matchers:
92107
name: 'Integration test (tflint_version: ${{ matrix.tflint_version }})'
93108
runs-on: ubuntu-latest

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ If version is `"latest"`, the action will get the latest version number using [O
1313

1414
Default: `"latest"`
1515

16+
### `checksums`
17+
18+
**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.
19+
20+
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.
21+
22+
**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).
23+
24+
1625
### `github_token`
1726

1827
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.
@@ -32,7 +41,7 @@ Default: `"false"`
3241
The following outputs are available when the `tflint_wrapper` input is enabled:
3342

3443
- `stdout` - The output (stdout) produced by the tflint command.
35-
- `stderr` - The error output (stdout) produced by the tflint command.
44+
- `stderr` - The error output (stderr) produced by the tflint command.
3645
- `exitcode` - The exit code produced by the tflint command.
3746

3847
## Usage
@@ -66,7 +75,6 @@ jobs:
6675
name: Setup TFLint
6776
with:
6877
tflint_version: v0.52.0
69-
7078
- name: Show version
7179
run: tflint --version
7280

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ inputs:
1313
description: Installs a wrapper script to wrap subsequent calls to `tflint` and expose `stdout`, `stderr`, and `exitcode` outputs
1414
default: 'false'
1515
required: false
16+
checksums:
17+
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.
18+
required: false
1619
outputs:
1720
stdout:
1821
description: The output (stdout) produced by the tflint command. Only available if `tflint_wrapper` is set to `true`.

dist/index.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9989,6 +9989,9 @@ function wrappy (fn, cb) {
99899989

99909990
const os = __nccwpck_require__(2037);
99919991
const path = __nccwpck_require__(1017);
9992+
const crypto = __nccwpck_require__(6113);
9993+
const fs = __nccwpck_require__(7147);
9994+
const { pipeline } = __nccwpck_require__(4845)
99929995

99939996
const core = __nccwpck_require__(2186);
99949997
const io = __nccwpck_require__(7436);
@@ -10041,10 +10044,29 @@ async function getTFLintVersion(inputVersion) {
1004110044
return inputVersion;
1004210045
}
1004310046

10044-
async function downloadCLI(url) {
10047+
async function fileSHA256(filePath) {
10048+
const hash = crypto.createHash('sha256');
10049+
const fileStream = fs.createReadStream(filePath);
10050+
10051+
await pipeline(fileStream, hash);
10052+
return hash.digest('hex');
10053+
}
10054+
10055+
async function downloadCLI(url, checksums) {
1004510056
core.debug(`Downloading tflint CLI from ${url}`);
1004610057
const pathToCLIZip = await tc.downloadTool(url);
1004710058

10059+
if (checksums.length > 0) {
10060+
core.debug('Verifying checksum of downloaded file');
10061+
10062+
const checksum = await fileSHA256(pathToCLIZip);
10063+
10064+
if (!checksums.includes(checksum)) {
10065+
throw new Error(`Mismatched checksum: expected one of ${checksums.join(', ')}, but got ${checksum}`);
10066+
}
10067+
core.debug('SHA256 hash verified successfully');
10068+
}
10069+
1004810070
core.debug('Extracting tflint CLI zip file');
1004910071
const pathToCLI = await tc.extractZip(pathToCLIZip);
1005010072
core.debug(`tflint CLI path is ${pathToCLI}.`);
@@ -10089,6 +10111,7 @@ async function installWrapper(pathToCLI) {
1008910111
async function run() {
1009010112
try {
1009110113
const inputVersion = core.getInput('tflint_version');
10114+
const checksums = core.getMultilineInput('checksums');
1009210115
const wrapper = core.getInput('tflint_wrapper') === 'true';
1009310116
const version = await getTFLintVersion(inputVersion);
1009410117
const platform = mapOS(os.platform());
@@ -10097,7 +10120,7 @@ async function run() {
1009710120
core.debug(`Getting download URL for tflint version ${version}: ${platform} ${arch}`);
1009810121
const url = `https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${platform}_${arch}.zip`;
1009910122

10100-
const pathToCLI = await downloadCLI(url);
10123+
const pathToCLI = await downloadCLI(url, checksums);
1010110124

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

1020910232
/***/ }),
1021010233

10234+
/***/ 4845:
10235+
/***/ ((module) => {
10236+
10237+
"use strict";
10238+
module.exports = require("stream/promises");
10239+
10240+
/***/ }),
10241+
1021110242
/***/ 1576:
1021210243
/***/ ((module) => {
1021310244

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "setup-tflint-action",
3-
"version": "2.0.0",
4-
"description": "Install and setup TFLint executable Action",
3+
"version": "2.1.0",
4+
"description": "Install and setup TFLint executable Action with SHA256 verification",
55
"main": "index.js",
66
"scripts": {
77
"lint": "eslint --fix . src test",
@@ -24,7 +24,9 @@
2424
"GitHub",
2525
"Actions",
2626
"TFLint",
27-
"Terraform"
27+
"Terraform",
28+
"SHA256",
29+
"Verification"
2830
],
2931
"license": "MIT",
3032
"bugs": {

src/setup-tflint.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const os = require('os');
22
const path = require('path');
3+
const crypto = require('crypto');
4+
const fs = require('fs');
5+
const { pipeline } = require('stream/promises')
36

47
const core = require('@actions/core');
58
const io = require('@actions/io');
@@ -52,10 +55,29 @@ async function getTFLintVersion(inputVersion) {
5255
return inputVersion;
5356
}
5457

55-
async function downloadCLI(url) {
58+
async function fileSHA256(filePath) {
59+
const hash = crypto.createHash('sha256');
60+
const fileStream = fs.createReadStream(filePath);
61+
62+
await pipeline(fileStream, hash);
63+
return hash.digest('hex');
64+
}
65+
66+
async function downloadCLI(url, checksums) {
5667
core.debug(`Downloading tflint CLI from ${url}`);
5768
const pathToCLIZip = await tc.downloadTool(url);
5869

70+
if (checksums.length > 0) {
71+
core.debug('Verifying checksum of downloaded file');
72+
73+
const checksum = await fileSHA256(pathToCLIZip);
74+
75+
if (!checksums.includes(checksum)) {
76+
throw new Error(`Mismatched checksum: expected one of ${checksums.join(', ')}, but got ${checksum}`);
77+
}
78+
core.debug('SHA256 hash verified successfully');
79+
}
80+
5981
core.debug('Extracting tflint CLI zip file');
6082
const pathToCLI = await tc.extractZip(pathToCLIZip);
6183
core.debug(`tflint CLI path is ${pathToCLI}.`);
@@ -100,6 +122,7 @@ async function installWrapper(pathToCLI) {
100122
async function run() {
101123
try {
102124
const inputVersion = core.getInput('tflint_version');
125+
const checksums = core.getMultilineInput('checksums');
103126
const wrapper = core.getInput('tflint_wrapper') === 'true';
104127
const version = await getTFLintVersion(inputVersion);
105128
const platform = mapOS(os.platform());
@@ -108,7 +131,7 @@ async function run() {
108131
core.debug(`Getting download URL for tflint version ${version}: ${platform} ${arch}`);
109132
const url = `https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${platform}_${arch}.zip`;
110133

111-
const pathToCLI = await downloadCLI(url);
134+
const pathToCLI = await downloadCLI(url, checksums);
112135

113136
if (wrapper) {
114137
await installWrapper(pathToCLI);

test/index.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const tc = require('@actions/tool-cache');
55

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

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

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

3231
test('add path should be called', async () => {
32+
jest.spyOn(core, 'addPath');
3333
await setup();
34-
3534
expect(core.addPath).toBeCalledTimes(1);
3635
});
3736
});

0 commit comments

Comments
 (0)