mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-08-18 02:56:31 +00:00
Add a GitHub Action for installing slsa-verifier. (#246)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
name: Actions pre submits
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
action-presubmit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
|
||||
|
||||
- name: Setup Node.js 16
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: actions/installer
|
||||
run: npm ci
|
||||
|
||||
- name: Run build and tests
|
||||
working-directory: actions/installer
|
||||
run: npm run all
|
||||
|
||||
- name: Compare the expected and actual dist/ directories
|
||||
working-directory: actions/installer
|
||||
id: diff
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
|
||||
# If there are uncommited changes in dist/, upload the directory
|
||||
- name: Upload dist/
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"i18n-text/no-en": "off",
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "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",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@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/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
lib
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
@@ -0,0 +1,21 @@
|
||||
# slsa-verifier setup GitHub Action
|
||||
|
||||
This action installs the SLSA verifier and adds it to your PATH.
|
||||
|
||||
For more information about `slsa-verifier`, refer to [its documentation](https://github.com/slsa-framework/slsa-verifier#verification-of-provenance).
|
||||
|
||||
For more information about SLSA in general, see [https://slsa.dev](https://slsa.dev).
|
||||
|
||||
## Usage
|
||||
|
||||
To install a specific version of `slsa-verifier`, use:
|
||||
|
||||
```yaml
|
||||
uses: slsa-framework/slsa-verifier-installer@v1.3.0
|
||||
```
|
||||
|
||||
See https://github.com/slsa-framework/slsa-verifier/releases for the list of available `slsa-verifier` releases.
|
||||
|
||||
For a full example workflow, see [../../.github/workflows/pre-submit.actions.yml](https://github.com/slsa-framework/slsa-verifier/.github/workflows/pre-submit.actions.yml).
|
||||
|
||||
This action requires using GitHub-provided Linux runners.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2022 SLSA Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: 'Install slsa-verifier'
|
||||
description: 'Installs SLSA verifier and adds it to your PATH'
|
||||
branding:
|
||||
icon: 'package'
|
||||
color: 'blue'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
|
||||
|
||||
- name: Setup Node.js 16
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: actions/installer
|
||||
shell: bash
|
||||
run: npm ci
|
||||
|
||||
- name: Run build
|
||||
working-directory: actions/installer
|
||||
shell: bash
|
||||
run: npm run build
|
||||
|
||||
- name: Run installer
|
||||
env:
|
||||
ACTION_REF: "${{ github.action_ref }}"
|
||||
TOKEN: "${{ github.token }}"
|
||||
REPOSITORY: "${{ github.repository }}"
|
||||
working-directory: actions/installer/dist
|
||||
shell: bash
|
||||
run: nodejs index.js
|
||||
Vendored
+13756
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+5297
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "setup-slsa-verifier",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"compile": "tsc",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"test": "jasmine",
|
||||
"package": "ncc build --source-map",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run compile && npm run package",
|
||||
"all": "npm run compile && npm run format && npm run lint && npm run test && npm run package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.9.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^5.0.3",
|
||||
"@actions/io": "^1.1.2",
|
||||
"@actions/tool-cache": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^4.3.0",
|
||||
"@types/node": "^18.7.13",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-github": "^4.3.7",
|
||||
"jasmine": "^4.3.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2022 SLSA Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const exec = require("@actions/exec");
|
||||
const index = require("../lib/index");
|
||||
const fs = require("fs");
|
||||
|
||||
describe("Test validVersion", function () {
|
||||
it("Accepts v0.0.1", function () {
|
||||
expect(index.validVersion("v1.0.0")).toBe(true);
|
||||
});
|
||||
it("Accepts v1.2.0", function () {
|
||||
expect(index.validVersion("v1.0.0")).toBe(true);
|
||||
});
|
||||
it("Rejects foobar", function () {
|
||||
expect(index.validVersion("foobar")).toBe(false);
|
||||
});
|
||||
it("Rejects commit hashes", function () {
|
||||
expect(
|
||||
index.validVersion(
|
||||
"1326430d044e8a9522c51e5f721e237b5f75acb6b4e518d129f669403cf7a79a"
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test fileHasExpectedSha256Hash", function () {
|
||||
it("throws an error when the file does not exist", function () {
|
||||
expect(function () {
|
||||
index.fileHasExpectedSha256Hash("/path/to/nowhere", "12345");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe("Tests accessing real files", function () {
|
||||
beforeEach(function () {
|
||||
this.tmpDir = fs.mkdtempSync("jasmine");
|
||||
this.testFile = `${this.tmpDir}/testfile`;
|
||||
fs.writeFileSync(this.testFile, "test data");
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
fs.rmSync(this.tmpDir, { recursive: true });
|
||||
});
|
||||
|
||||
it("Returns false when the computed and expected hashes don't match", function () {
|
||||
expect(index.fileHasExpectedSha256Hash(this.testFile, "foobar")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
it("Returns true when the computed and expected hashes don't match", function () {
|
||||
expect(
|
||||
index.fileHasExpectedSha256Hash(
|
||||
this.testFile,
|
||||
"916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9"
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": [
|
||||
"**/*.test.?(m)ts"
|
||||
],
|
||||
"helpers": [
|
||||
"helpers/**/*.?(m)js"
|
||||
],
|
||||
"env": {
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright 2022 SLSA Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
const BOOTSTRAP_VERSION = "v1.3.0";
|
||||
const BOOTSTRAP_VERIFIER_SHA256 =
|
||||
"1326430d044e8a9522c51e5f721e237b5f75acb6b4e518d129f669403cf7a79a";
|
||||
const BINARY_NAME = "slsa-verifier";
|
||||
const PROVENANCE_NAME = "slsa-verifier-linux-amd64.intoto.jsonl";
|
||||
|
||||
// If true, the input string conforms to slsa-verifier's versioning system.
|
||||
export function validVersion(version: string): boolean {
|
||||
const re = /(v[0-9]+\.[0-9]+\.[0-9]+)/;
|
||||
return re.test(version);
|
||||
}
|
||||
|
||||
// Resolve command line argument to a version number
|
||||
export async function getVerifierVersion(actionRef: string): Promise<string> {
|
||||
if (validVersion(actionRef)) {
|
||||
return actionRef;
|
||||
}
|
||||
|
||||
// If actionRef is a commit SHA, then find the associated version number.
|
||||
const shaRe = /^[a-f\d]{40}$/;
|
||||
if (shaRe.test(actionRef)) {
|
||||
const octokit = github.getOctokit(process.env.TOKEN || "");
|
||||
const { data: releases } = await octokit.request(
|
||||
"GET /repos/{repository}/releases",
|
||||
{
|
||||
repository: process.env.REPOSITORY,
|
||||
}
|
||||
);
|
||||
for (const release of releases) {
|
||||
const { data: commit } = await octokit.request(
|
||||
"GET /reps/{repository}/git/ref/tags/{tagName}",
|
||||
{
|
||||
repository: process.env.REPOSITORY,
|
||||
tagName: release.tag_name,
|
||||
}
|
||||
);
|
||||
const commitSha = commit.object.sha;
|
||||
if (commitSha === actionRef) {
|
||||
return release.tag_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid version provided: ${actionRef}. For the set of valid versions, see https://github.com/slsa-framework/slsa-verifier/releases.`
|
||||
);
|
||||
}
|
||||
|
||||
// If true, then the file in `path` has the same SHA256 hash as `expectedSha256Hash``.
|
||||
export function fileHasExpectedSha256Hash(
|
||||
filePath: string,
|
||||
expectedSha256Hash: string
|
||||
): boolean {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
const untrustedFile = fs.readFileSync(filePath);
|
||||
const computedSha256Hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(untrustedFile)
|
||||
.digest("hex");
|
||||
return computedSha256Hash === expectedSha256Hash;
|
||||
}
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
// Delete bootstrap version and maybe installed version
|
||||
async function cleanup(): Promise<void> {
|
||||
await io.rmRF(`${tmpDir}`);
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
// Get requested verifier version and validate
|
||||
const actionRef = process.env.ACTION_REF || "";
|
||||
let version: string;
|
||||
try {
|
||||
version = await getVerifierVersion(actionRef);
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(
|
||||
`Invalid version provided. For the set of valid versions, see https://github.com/slsa-framework/slsa-verifier/releases. ${errMsg}`
|
||||
);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "slsa-verifier_"));
|
||||
const bootstrapDir = `${tmpDir}/bootstrap`;
|
||||
const installDir = `${tmpDir}/${version}`;
|
||||
|
||||
let bootstrapVerifierPath;
|
||||
try {
|
||||
// Download bootstrap version and validate SHA256 checksum
|
||||
bootstrapVerifierPath = await tc.downloadTool(
|
||||
`https://github.com/slsa-framework/slsa-verifier/releases/download/${BOOTSTRAP_VERSION}/slsa-verifier-linux-amd64`,
|
||||
`${bootstrapDir}/${BINARY_NAME}`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Error downloading bootstrap slsa-verifier: ${errMsg}`);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!fileHasExpectedSha256Hash(bootstrapVerifierPath, BOOTSTRAP_VERIFIER_SHA256)
|
||||
) {
|
||||
core.setFailed(
|
||||
`Unable to verify slsa-verifier checksum. Aborting installation.`
|
||||
);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
fs.chmodSync(bootstrapVerifierPath, 0o100);
|
||||
|
||||
let downloadedBinaryPath;
|
||||
try {
|
||||
// Download requested version binary and provenance
|
||||
downloadedBinaryPath = await tc.downloadTool(
|
||||
`https://github.com/slsa-framework/slsa-verifier/releases/download/${version}/slsa-verifier-linux-amd64`,
|
||||
`${installDir}/${BINARY_NAME}`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Error downloading slsa-verifier: ${errMsg}`);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
let downloadedProvenancePath;
|
||||
try {
|
||||
downloadedProvenancePath = await tc.downloadTool(
|
||||
`https://github.com/slsa-framework/slsa-verifier/releases/download/${version}/slsa-verifier-linux-amd64.intoto.jsonl`,
|
||||
`${installDir}/${PROVENANCE_NAME}`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Error downloading binary provenance: ${errMsg}`);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate binary provenance
|
||||
try {
|
||||
const { exitCode, stdout, stderr } = await exec.getExecOutput(
|
||||
`${bootstrapVerifierPath}`,
|
||||
[
|
||||
`-artifact-path=${downloadedBinaryPath}`,
|
||||
`-provenance=${downloadedProvenancePath}`,
|
||||
`-source=github.com/slsa-framework/slsa-verifier`,
|
||||
`-tag=${version}`,
|
||||
]
|
||||
);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Unable to verify binary provenance. Aborting installation. stdout: ${stdout}; stderr: ${stderr}`
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Error executing slsa-verifier: ${errMsg}`);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy requested version to HOME directory.
|
||||
const finalDir = `${os.homedir()}/.slsa/bin/${version}`;
|
||||
const finalPath = `${finalDir}/${BINARY_NAME}`;
|
||||
|
||||
fs.mkdirSync(finalDir, { recursive: true });
|
||||
fs.copyFileSync(downloadedBinaryPath, finalPath);
|
||||
fs.chmodSync(finalPath, 0o100);
|
||||
core.addPath(finalDir);
|
||||
core.setOutput("verifier-path", finalDir);
|
||||
|
||||
cleanup();
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6", /* 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'. */
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user