Compare commits

..

1 Commits

Author SHA1 Message Date
Brian DeHamer 740d40239e wip
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-09-18 15:26:14 -07:00
31 changed files with 93185 additions and 85 deletions
+4
View File
@@ -0,0 +1,4 @@
lib/
dist/
node_modules/
coverage/
+17
View File
@@ -9,3 +9,20 @@ updates:
update-types:
- minor
- patch
ignore:
- dependency-name: 'actions/attest-build-provenance'
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
groups:
npm-development:
dependency-type: development
update-types:
- minor
- patch
npm-production:
dependency-type: production
update-types:
- patch
+83
View File
@@ -0,0 +1,83 @@
env:
node: true
es6: true
jest: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- '!.*'
- '**/node_modules/.*'
- '**/dist/.*'
- '**/coverage/.*'
- '*.json'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2023
sourceType: module
project:
- './.github/linters/tsconfig.json'
- './tsconfig.json'
plugins:
- jest
- '@typescript-eslint'
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:github/recommended
- plugin:jest/recommended
rules:
{
'camelcase': 'off',
'eslint-comments/no-use': 'off',
'eslint-comments/no-unused-disable': 'off',
'i18n-text/no-en': 'off',
'import/no-namespace': 'off',
'no-console': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'semi': 'off',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/explicit-member-accessibility':
['error', { 'accessibility': 'no-public' }],
'@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-require-imports': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': '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',
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/unbound-method': 'error'
}
+7
View File
@@ -0,0 +1,7 @@
# Unordered list style
MD004:
style: dash
# Ordered list item prefix
MD029:
style: one
+10
View File
@@ -0,0 +1,10 @@
rules:
document-end: disable
document-start:
level: warning
present: false
line-length:
level: warning
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["../../__tests__/**/*", "../../src/**/*"],
"exclude": ["../../dist", "../../node_modules", "../../coverage", "*.json"]
}
+66
View File
@@ -0,0 +1,66 @@
# In TypeScript actions, `dist/` is a special directory. When you reference
# an action with the `uses:` property, `dist/index.js` is the code that will be
# run. For this project, the `dist/index.js` file is transpiled from other
# source files. This workflow ensures the `dist/` directory contains the
# expected transpiled code.
#
# If this workflow is run from a feature branch, it will act as an additional CI
# check and fail if the checked-in `dist/` directory does not match what is
# expected from the build.
name: Check Transpiled JavaScript
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
jobs:
check-dist:
name: Check dist/
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Build dist/ Directory
id: build
run: npm run bundle
# This will fail the workflow if the PR wasn't created by Dependabot.
- name: Compare Directories
id: diff
run: |
if [ "$(git diff --ignore-space-at-eol --text dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff --ignore-space-at-eol --text dist/
exit 1
fi
# If `dist/` was different than expected, and this was not a Dependabot
# PR, upload the expected version as a workflow artifact.
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
name: Upload Artifact
id: upload
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
+39 -5
View File
@@ -7,11 +7,45 @@ on:
push:
branches:
- main
- "releases/*"
- 'releases/*'
permissions: {}
jobs:
test-typescript:
name: TypeScript Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
id: checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: npm-ci
run: npm ci
- name: Check Format
id: npm-format-check
run: npm run format:check
- name: Lint
id: npm-lint
run: npm run lint
- name: Test
id: npm-ci-test
run: npm run ci-test
test-attest-provenance:
name: Test attest-provenance action
runs-on: ubuntu-latest
@@ -23,15 +57,15 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run attest-provenance
id: attest-provenance
uses: ./
env:
INPUT_PRIVATE-SIGNING: "true"
INPUT_PRIVATE-SIGNING: 'true'
with:
subject-digest: "sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32"
subject-name: "subject"
subject-digest: 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
subject-name: 'subject'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Dump output
run: jq < ${{ steps.attest-provenance.outputs.bundle-path }}
+50
View File
@@ -0,0 +1,50 @@
name: CodeQL
on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
- cron: '31 7 * * 3'
permissions: {}
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- TypeScript
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
id: initialize
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
source-root: src
- name: Autobuild
id: autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@v3
+51
View File
@@ -0,0 +1,51 @@
name: Lint Codebase
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
packages: read
statuses: write
jobs:
lint:
name: Lint Codebase
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Lint Codebase
id: super-linter
uses: super-linter/super-linter/slim@v7
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_ALL_CODEBASE: true
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_TYPESCRIPT_STANDARD: false
VALIDATE_JSCPD: false
VALIDATE_YAML_PRETTIER: false
+17
View File
@@ -0,0 +1,17 @@
name: GitHub Sigstore Prober
on:
workflow_dispatch:
schedule:
# run every 5 minutes, as often as Github Actions allows
- cron: '*/5 * * * *'
jobs:
prober:
permissions:
attestations: write
id-token: write
secrets: inherit
uses: ./.github/workflows/prober.yml
with:
sigstore: github
+17
View File
@@ -0,0 +1,17 @@
name: Public-Good Sigstore Prober
on:
workflow_dispatch:
schedule:
# run every 5 minutes, as often as Github Actions allows
- cron: '*/5 * * * *'
jobs:
prober:
permissions:
attestations: write
id-token: write
secrets: inherit
uses: ./.github/workflows/prober.yml
with:
sigstore: public-good
+100
View File
@@ -0,0 +1,100 @@
name: Prober Workflow
on:
workflow_call:
inputs:
sigstore:
description: 'Which Sigstore instance to use for signing'
default: 'public-good'
required: false
type: string
secrets:
trust-domain:
description: 'Trust domain in which the test is executed'
required: true
type: string
service:
description: 'Service against which status should be reported'
required: true
type: string
team:
description: 'Team associated with status report'
required: true
type: string
jobs:
probe:
runs-on: ubuntu-latest
permissions:
attestations: write
id-token: write
steps:
- uses: hmarr/debug-action@v2
- name: Request OIDC Token
run: |
curl "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=nobody" \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
-H "Accept: application/json; api-version=2.0" \
-H "Content-Type: application/json" \
--silent | jq -r '.value' | jq -R 'split(".") | .[0],.[1] | @base64d | fromjson'
- name: Create artifact
run: |
date > artifact
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
path: "artifact"
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
env:
INPUT_PRIVATE-SIGNING: ${{ inputs.sigstore == 'github' && 'true' || 'false' }}
with:
subject-path: artifact
- name: Verify build artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
gh attestation verify ./artifact --owner "$GITHUB_REPOSITORY_OWNER"
- name: Report attestation prober success
if: ${{ success() }}
uses: masci/datadog@a5d283e78e33a688ed08a96ba64440505e645a8c # v1.7.1
with:
api-key: "${{ secrets.DATADOG_API_KEY }}"
service-checks: |
- check: "attestation-integration.actions.prober"
status: 0
host_name: github.com
tags:
- "catalog_service:${{ secrets.service }}"
- "service:${{ secrets.service }}"
- "stamp:${{ secrets.trust-domain }}"
- "env:production"
- "repo:${{ github.repository }}"
- "team:${{ secrets.team }}"
- "sigstore:${{ inputs.sigstore }}"
- name: Report attestation prober failure
if: ${{ failure() }}
uses: masci/datadog@a5d283e78e33a688ed08a96ba64440505e645a8c # v1.7.1
with:
api-key: "${{ secrets.DATADOG_API_KEY }}"
service-checks: |
- check: "attestation-integration.actions.prober"
message: "${{ github.repository_owner }} failed prober check"
status: 2
host_name: github.com
tags:
- "catalog_service:${{ secrets.service }}"
- "service:${{ secrets.service }}"
- "stamp:${{ secrets.trust-domain }}"
- "env:production"
- "repo:${{ github.repository }}"
- "team:${{ secrets.team }}"
- "sigstore:${{ inputs.sigstore }}"
+1
View File
@@ -0,0 +1 @@
20.6.0
+3
View File
@@ -0,0 +1,3 @@
dist/
node_modules/
coverage/
+16
View File
@@ -0,0 +1,16 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "avoid",
"proseWrap": "always",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
+214 -19
View File
@@ -19,30 +19,221 @@ initiated.
Attestations can be verified using the [`attestation` command in the GitHub
CLI][5].
See [Using artifact attestations to establish provenance for builds][6] for more
See [Using artifact attestations to establish provenance for builds][9] for more
information on artifact attestations.
<!-- prettier-ignore-start -->
> [!NOTE]
> Artifact attestations are available in public repositories for all
> current GitHub plans. They are not available on legacy plans, such as Bronze,
> Silver, or Gold. If you are on a GitHub Free, GitHub Pro, or GitHub Team plan,
> artifact attestations are only available for public repositories. To use
> artifact attestations in private or internal repositories, you must be on a
> GitHub Enterprise Cloud plan.
<!-- prettier-ignore-end -->
## Usage
**As of version 4, `actions/attest-build-provenance` is simply a wrapper on top
of [`actions/attest`][7].**
Within the GitHub Actions workflow which builds some artifact you would like to
attest:
Existing applications may continue to use the `attest-build-provenance` action,
but new implementations should use `actions/attest` instead. Please see the
[`actions/attest`][7] repository for usage information.
1. Ensure that the following permissions are set:
Documentation for previous versions of this action can be found
[here](https://github.com/actions/attest-build-provenance/blob/v3.2.0/README.md).
```yaml
permissions:
id-token: write
attestations: write
```
The `id-token` permission gives the action the ability to mint the OIDC token
necessary to request a Sigstore signing certificate. The `attestations`
permission is necessary to persist the attestation.
1. Add the following to your workflow after your artifact has been built:
```yaml
- uses: actions/attest-build-provenance@v1
with:
subject-path: '<PATH TO ARTIFACT>'
```
The `subject-path` parameter should identify the artifact for which you want
to generate an attestation.
### Inputs
See [action.yml](action.yml)
```yaml
- uses: actions/attest-build-provenance@v1
with:
# Path to the artifact serving as the subject of the attestation. Must
# specify exactly one of "subject-path" or "subject-digest". May contain a
# glob pattern or list of paths (total subject count cannot exceed 2500).
subject-path:
# SHA256 digest of the subject for the attestation. Must be in the form
# "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
# of "subject-path" or "subject-digest".
subject-digest:
# Subject name as it should appear in the attestation. Required unless
# "subject-path" is specified, in which case it will be inferred from the
# path.
subject-name:
# Whether to push the attestation to the image registry. Requires that the
# "subject-name" parameter specify the fully-qualified image name and that
# the "subject-digest" parameter be specified. Defaults to false.
push-to-registry:
# Whether to attach a list of generated attestations to the workflow run
# summary page. Defaults to true.
show-summary:
# The GitHub token used to make authenticated API requests. Default is
# ${{ github.token }}
github-token:
```
### Outputs
<!-- markdownlint-disable MD013 -->
| Name | Description | Example |
| ------------- | -------------------------------------------------------------- | ------------------------ |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.jsonl` |
<!-- markdownlint-enable MD013 -->
Attestations are saved in the JSON-serialized [Sigstore bundle][6] format.
If multiple subjects are being attested at the same time, each attestation will
be written to the output file on a separate line (using the [JSON Lines][7]
format).
## Attestation Limits
### Subject Limits
No more than 2500 subjects can be attested at the same time. Subjects will be
processed in batches 50. After the initial group of 50, each subsequent batch
will incur an exponentially increasing amount of delay (capped at 1 minute of
delay per batch) to avoid overwhelming the attestation API.
## Examples
### Identify Subject by Path
For the basic use case, simply add the `attest-build-provenance` action to your
workflow and supply the path to the artifact for which you want to generate
attestation.
```yaml
name: build-attest
on:
workflow_dispatch:
jobs:
build:
permissions:
id-token: write
contents: read
attestations: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build artifact
run: make my-app
- name: Attest
uses: actions/attest-build-provenance@v1
with:
subject-path: '${{ github.workspace }}/my-app'
```
### Identify Multiple Subjects
If you are generating multiple artifacts, you can generate a provenance
attestation for each by using a wildcard in the `subject-path` input.
```yaml
- uses: actions/attest-build-provenance@v1
with:
subject-path: 'dist/**/my-bin-*'
```
For supported wildcards along with behavior and documentation, see
[@actions/glob][8] which is used internally to search for files.
Alternatively, you can explicitly list multiple subjects with either a comma or
newline delimited list:
```yaml
- uses: actions/attest-build-provenance@v1
with:
subject-path: 'dist/foo, dist/bar'
```
```yaml
- uses: actions/attest-build-provenance@v1
with:
subject-path: |
dist/foo
dist/bar
```
### Container Image
When working with container images you can invoke the action with the
`subject-name` and `subject-digest` inputs.
If you want to publish the attestation to the container registry with the
`push-to-registry` option, it is important that the `subject-name` specify the
fully-qualified image name (e.g. "ghcr.io/user/app" or
"acme.azurecr.io/user/app"). Do NOT include a tag as part of the image name --
the specific image being attested is identified by the supplied digest.
Attestation bundles are stored in the OCI registry according to the [Cosign
Bundle Specification][10].
> **NOTE**: When pushing to Docker Hub, please use "index.docker.io" as the
> registry portion of the image name.
```yaml
name: build-attested-image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
packages: write
contents: read
attestations: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
id: push
uses: docker/build-push-action@v5.0.0
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Attest
uses: actions/attest-build-provenance@v1
id: attest
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
```
[1]: https://github.com/actions/toolkit/tree/main/packages/attest
[2]: https://github.com/in-toto/attestation/tree/main/spec/v1
@@ -50,5 +241,9 @@ Documentation for previous versions of this action can be found
[4]: https://www.sigstore.dev/
[5]: https://cli.github.com/manual/gh_attestation_verify
[6]:
https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto
[7]: https://jsonlines.org/
[8]: https://github.com/actions/toolkit/tree/main/packages/glob#patterns
[9]:
https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
[7]: https://github.com/actions/attest
[10]: https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md
+19
View File
@@ -3,6 +3,25 @@
Follow the steps below to tag a new release for the
`actions/attest-build-provenance` action.
If changes were made to the internal `actions/attest-build-provenance/predicate`
action (any updates to [`./predicate/action.yaml`](./predicate/action.yml) or
any of the code in the [`./src`](./src) directory), start with step #1;
otherwise, skip directly to step #5.
1. Merge the latest changes to the `main` branch.
1. Create and push a new predicate tag of the form `predicate@X.X.X` following
SemVer conventions:
```shell
git tag -a "predicate@X.X.X" -m "predicate@X.X.X Release"
git push --tags
```
1. Update the reference to the `actions/attest-build-provenance/predicate`
action in [`action.yml`](./action.yml) to point to the SHA of the newly
created tag.
1. Push the `action.yml` change and open a PR. Once it has been reviewed, merge
the PR and proceed with the release instructions.
1. Create a new release for the top-level action using a tag of the form
`vX.X.X` following SemVer conventions:
+79
View File
@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`main when a non-default OIDC issuer is used successfully run main 1`] = `
{
"buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
"ref": "main",
"repository": "https://example-01.ghe.com/owner/repo",
},
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
"runner_environment": "github-hosted",
},
},
"resolvedDependencies": [
{
"digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
},
"uri": "git+https://example-01.ghe.com/owner/repo@refs/heads/main",
},
],
},
"runDetails": {
"builder": {
"id": "https://example-01.ghe.com/owner/shared/.github/workflows/build.yml@main",
},
"metadata": {
"invocationId": "https://example-01.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
},
},
}
`;
exports[`main when the default OIDC issuer is used successfully run main 1`] = `
{
"buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
"ref": "main",
"repository": "https://github.com/owner/repo",
},
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
"runner_environment": "github-hosted",
},
},
"resolvedDependencies": [
{
"digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
},
"uri": "git+https://github.com/owner/repo@refs/heads/main",
},
],
},
"runDetails": {
"builder": {
"id": "https://github.com/owner/shared/.github/workflows/build.yml@main",
},
"metadata": {
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
},
},
}
`;
+17
View File
@@ -0,0 +1,17 @@
/**
* Unit tests for the action's entrypoint, src/index.ts
*/
import * as main from '../src/main'
// Mock the action's entrypoint
const runMock = jest.spyOn(main, 'run').mockImplementation()
describe('index', () => {
it('calls run when imported', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../src/index')
expect(runMock).toHaveBeenCalled()
})
})
+157
View File
@@ -0,0 +1,157 @@
import * as core from '@actions/core'
import * as jose from 'jose'
import nock from 'nock'
import * as main from '../src/main'
// Mock the GitHub Actions core library functions
const setOutputMock = jest.spyOn(core, 'setOutput')
const setFailedMock = jest.spyOn(core, 'setFailed')
// Ensure that setFailed doesn't set an exit code during tests
setFailedMock.mockImplementation(() => {})
describe('main', () => {
let outputs = {} as Record<string, string>
const originalEnv = process.env
beforeEach(() => {
jest.resetAllMocks()
setOutputMock.mockImplementation((key, value) => {
outputs[key] = value
})
})
afterEach(() => {
outputs = {}
process.env = originalEnv
})
describe('when the default OIDC issuer is used', () => {
const issuer = 'https://token.actions.githubusercontent.com'
const audience = 'nobody'
const jwksPath = '/.well-known/jwks.json'
const tokenPath = '/token'
const claims = {
iss: issuer,
aud: 'nobody',
repository: 'owner/repo',
ref: 'refs/heads/main',
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
job_workflow_ref: 'owner/shared/.github/workflows/build.yml@main',
event_name: 'push',
repository_id: 'repo-id',
repository_owner_id: 'owner-id',
run_id: 'run-id',
run_attempt: 'run-attempt',
runner_environment: 'github-hosted'
}
beforeEach(async () => {
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_REPOSITORY: claims.repository
}
// Generate JWT signing key
const key = await jose.generateKeyPair('PS256')
// Create JWK, JWKS, and JWT
const kid = '12345'
const jwk = await jose.exportJWK(key.publicKey)
const jwks = { keys: [{ ...jwk, kid }] }
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({ alg: 'PS256', kid })
.sign(key.privateKey)
// Mock OpenID configuration and JWKS endpoints
nock(issuer)
.get('/.well-known/openid-configuration')
.reply(200, { jwks_uri: `${issuer}${jwksPath}` })
nock(issuer).get(jwksPath).reply(200, jwks)
// Mock OIDC token endpoint for populating the provenance
nock(issuer).get(tokenPath).query({ audience }).reply(200, { value: jwt })
})
it('successfully run main', async () => {
// Run the main function
await main.run()
// Verify that outputs were set correctly
expect(setOutputMock).toHaveBeenCalledTimes(2)
expect(outputs['predicate']).toMatchSnapshot()
expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1')
})
})
describe('when a non-default OIDC issuer is used', () => {
const issuer = 'https://token.actions.example-01.ghe.com'
const audience = 'nobody'
const jwksPath = '/.well-known/jwks.json'
const tokenPath = '/token'
const claims = {
iss: issuer,
aud: 'nobody',
repository: 'owner/repo',
ref: 'refs/heads/main',
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
job_workflow_ref: 'owner/shared/.github/workflows/build.yml@main',
event_name: 'push',
repository_id: 'repo-id',
repository_owner_id: 'owner-id',
run_id: 'run-id',
run_attempt: 'run-attempt',
runner_environment: 'github-hosted'
}
beforeEach(async () => {
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
GITHUB_SERVER_URL: 'https://example-01.ghe.com',
GITHUB_REPOSITORY: claims.repository
}
// Generate JWT signing key
const key = await jose.generateKeyPair('PS256')
// Create JWK, JWKS, and JWT
const kid = '12345'
const jwk = await jose.exportJWK(key.publicKey)
const jwks = { keys: [{ ...jwk, kid }] }
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({ alg: 'PS256', kid })
.sign(key.privateKey)
// Mock OpenID configuration and JWKS endpoints
nock(issuer)
.get('/.well-known/openid-configuration')
.reply(200, { jwks_uri: `${issuer}${jwksPath}` })
nock(issuer).get(jwksPath).reply(200, jwks)
// Mock OIDC token endpoint for populating the provenance
nock(issuer).get(tokenPath).query({ audience }).reply(200, { value: jwt })
})
it('successfully run main', async () => {
// Run the main function
await main.run()
// Verify that outputs were set correctly
expect(setOutputMock).toHaveBeenCalledTimes(2)
expect(outputs['predicate']).toMatchSnapshot()
expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1')
})
})
})
+19 -61
View File
@@ -1,51 +1,28 @@
name: "Attest Build Provenance"
description: "Generate provenance attestations for build artifacts"
author: "GitHub"
name: 'Attest Build Provenance'
description: 'Generate provenance attestations for build artifacts'
author: 'GitHub'
branding:
color: "blue"
icon: "lock"
color: 'blue'
icon: 'lock'
inputs:
subject-path:
description: >
Path to the artifact serving as the subject of the attestation. Must
specify exactly one of "subject-path", "subject-digest", or
"subject-checksums". May contain a glob pattern or list of paths (total
subject count cannot exceed 1024).
specify exactly one of "subject-path" or "subject-digest". May contain a
glob pattern or list of paths (total subject count cannot exceed 2500).
required: false
subject-digest:
description: >
Digest of the subject for which provenance will be generated. Must be in
the form "algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify
exactly one of "subject-path", "subject-digest", or "subject-checksums".
exactly one of "subject-path" or "subject-digest".
required: false
subject-name:
description: >
Subject name as it should appear in the attestation. Required when
identifying the subject with the "subject-digest" input.
subject-checksums:
description: >
Path to checksums file containing digest and name of subjects for
attestation. Must specify exactly one of "subject-path", "subject-digest",
or "subject-checksums".
required: false
predicate-type:
description: >
URI identifying the type of the predicate. Required when using "predicate"
or "predicate-path" for custom attestations.
required: false
predicate:
description: >
String containing the value for the attestation predicate. String length
cannot exceed 16MB. Must supply exactly one of "predicate-path" or
"predicate" when creating custom attestations.
required: false
predicate-path:
description: >
Path to the file which contains the content for the attestation predicate.
File size cannot exceed 16MB. Must supply exactly one of "predicate-path"
or "predicate" when creating custom attestations.
required: false
Subject name as it should appear in the provenance statement. Required
unless "subject-path" is specified, in which case it will be inferred from
the path.
push-to-registry:
description: >
Whether to push the provenance statement to the image registry. Requires
@@ -53,12 +30,6 @@ inputs:
and that the "subject-digest" parameter be specified. Defaults to false.
default: false
required: false
create-storage-record:
description: >
Whether to create a storage record for the artifact. Requires that
push-to-registry is set to true. Defaults to true.
default: true
required: false
show-summary:
description: >
Whether to attach a list of generated attestations to the workflow run
@@ -73,35 +44,22 @@ inputs:
outputs:
bundle-path:
description: "The path to the file containing the attestation bundle."
description: 'The path to the file containing the attestation bundle(s).'
value: ${{ steps.attest.outputs.bundle-path }}
attestation-id:
description: "The ID of the attestation."
value: ${{ steps.attest.outputs.attestation-id }}
attestation-url:
description: "The URL for the attestation summary."
value: ${{ steps.attest.outputs.attestation-url }}
storage-record-ids:
description: "GitHub IDs for the storage records"
value: ${{ steps.attest.outputs.storage-record-ids }}
runs:
using: "composite"
using: 'composite'
steps:
- name: Attest
- uses: actions/attest-build-provenance/predicate@f1185f1959cdaeda41a7f5a7b43cbe6b58a7a793 # predicate@1.1.3
id: generate-build-provenance-predicate
- uses: actions/attest@67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d # v1.4.1
id: attest
env:
NODE_OPTIONS: "--max-http-header-size=32768"
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-path: ${{ inputs.subject-path }}
subject-name: ${{ inputs.subject-name }}
subject-digest: ${{ inputs.subject-digest }}
subject-checksums: ${{ inputs.subject-checksums }}
predicate-type: ${{ inputs.predicate-type }}
predicate: ${{ inputs.predicate }}
predicate-path: ${{ inputs.predicate-path }}
subject-name: ${{ inputs.subject-name }}
predicate-type: ${{ steps.generate-build-provenance-predicate.outputs.predicate-type }}
predicate: ${{ steps.generate-build-provenance-predicate.outputs.predicate }}
push-to-registry: ${{ inputs.push-to-registry }}
create-storage-record: ${{ inputs.create-storage-record }}
show-summary: ${{ inputs.show-summary }}
github-token: ${{ inputs.github-token }}
Generated Vendored
+81131
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+2519
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
process.stdout.write = jest.fn()
+8383
View File
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
{
"name": "actions/attest-build-provenance",
"description": "Generate signed build provenance attestations",
"version": "1.1.3",
"author": "",
"private": true,
"homepage": "https://github.com/actions/attest-build-provenance",
"repository": {
"type": "git",
"url": "git+https://github.com/actions/attest-build-provenance.git"
},
"bugs": {
"url": "https://github.com/actions/attest-build-provenance/issues"
},
"keywords": [
"actions",
"attestation",
"provenance"
],
"exports": {
".": "./dist/index.js"
},
"engines": {
"node": ">=20"
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "jest",
"format:write": "prettier --write **/*.ts",
"format:check": "prettier --check **/*.ts",
"lint:eslint": "npx eslint . -c ./.github/linters/.eslintrc.yml",
"lint:markdown": "npx markdownlint --config .github/linters/.markdown-lint.yml \"*.md\"",
"lint": "npm run lint:eslint && npm run lint:markdown",
"package": "ncc build src/index.ts --license licenses.txt",
"package:watch": "npm run package -- --watch",
"test": "jest",
"all": "npm run format:write && npm run lint && npm run test && npm run package"
},
"license": "MIT",
"jest": {
"preset": "ts-jest",
"verbose": true,
"clearMocks": true,
"testEnvironment": "node",
"moduleFileExtensions": [
"js",
"ts"
],
"setupFilesAfterEnv": [
"./jest.setup.js"
],
"testMatch": [
"**/*.test.ts"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
"coverageReporters": [
"json-summary",
"text",
"lcov"
],
"collectCoverage": true,
"collectCoverageFrom": [
"./src/**"
]
},
"dependencies": {
"@actions/attest": "^1.4.2",
"@actions/core": "^1.10.1"
},
"devDependencies": {
"@types/jest": "^29.5.13",
"@types/node": "^22.5.5",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.18.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0",
"eslint-plugin-github": "^5.0.2",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"jose": "^5.9.2",
"markdownlint-cli": "^0.41.0",
"nock": "^13.5.5",
"prettier": "^3.3.3",
"prettier-eslint": "^16.3.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2"
}
}
+14
View File
@@ -0,0 +1,14 @@
name: 'Build Provenance Predicate'
description: 'Generate predicate for build provenance attestations'
author: 'GitHub'
outputs:
predicate:
description: >
The JSON-serialized of the attestation predicate.
predicate-type:
description: >
URI identifying the type of the predicate.
runs:
using: node20
main: ../dist/index.js
+7
View File
@@ -0,0 +1,7 @@
/**
* The entrypoint for the action.
*/
import { run } from './main'
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run()
+20
View File
@@ -0,0 +1,20 @@
import { buildSLSAProvenancePredicate } from '@actions/attest'
import * as core from '@actions/core'
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
try {
// Calculate subject from inputs and generate provenance
const predicate = await buildSLSAProvenancePredicate()
core.setOutput('predicate', predicate.params)
core.setOutput('predicate-type', predicate.type)
} catch (err) {
const error = err instanceof Error ? err : new Error(`${err}`)
// Fail the workflow run if an error occurs
core.setFailed(error.message)
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"rootDir": "./src",
"moduleResolution": "NodeNext",
"baseUrl": "./",
"sourceMap": true,
"outDir": "./dist",
"noImplicitAny": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"newLine": "lf"
},
"exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
}