Compare commits

..
Author SHA1 Message Date
Brian DeHamer 429e2ac7fe skip attestation store
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-31 10:07:45 -07:00
30 changed files with 37084 additions and 113 deletions
+4
View File
@@ -0,0 +1,4 @@
lib/
dist/
node_modules/
coverage/
+16 -1
View File
@@ -10,4 +10,19 @@ updates:
- minor
- patch
ignore:
- dependency-name: 'actions/attest-sbom'
- dependency-name: "actions/attest-sbom"
- 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'
}
+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/
+36 -2
View File
@@ -12,8 +12,42 @@ on:
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-sbom:
name: Test attest-sbom action with local sbom file
name: Test attest-sbom action with local sbom file
runs-on: ubuntu-latest
permissions:
attestations: write
@@ -23,7 +57,7 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run attest-sbom
id: attest-sbom
uses: ./
+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
+50
View File
@@ -0,0 +1,50 @@
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@v6
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_JSCPD: false
VALIDATE_GITHUB_ACTIONS: false
+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"
}
+201 -27
View File
@@ -1,12 +1,5 @@
# `actions/attest-sbom`
<!-- prettier-ignore-start -->
> [!WARNING]
> This action is being deprecated in favor of [`actions/attest`][9]. `actions/attest-sbom` will continue to function as a wrapper on top of `actions/attest` for some period of time, but applications should make plans to migrate.
>
> All of the existing action inputs are compatible with the `actions/attest` interface.
<!-- prettier-ignore-end -->
Generate signed SBOM attestations for workflow artifacts. Internally powered by
the [@actions/attest][1] package.
@@ -28,27 +21,208 @@ initiated.
Attestations can be verified using the [`attestation` command in the GitHub
CLI][7].
See [Using artifact attestations to establish provenance for builds][8] 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 -->
See [Using artifact attestations to establish provenance for builds][11]
for more information on artifact attestations.
## Usage
**As of version 4, `actions/attest-sbom` is simply a wrapper on top of
[`actions/attest`][9].**
Within the GitHub Actions workflow which builds some artifact you would like to
attest:
Please see the [`actions/attest`][9] repository for usage information.
1. Ensure that the following permissions are set:
Documentation for previous versions of this action can be found [here][10].
```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 and
your SBOM has been generated:
```yaml
- uses: actions/attest-sbom@v1
with:
subject-path: '<PATH TO ARTIFACT>'
sbom-path: '<PATH TO SBOM>'
```
The `subject-path` parameter should identify the artifact for which you want
to generate an SBOM attestation. The `sbom-path` parameter should identify
the SBOM document to be associated with the subject.
### Inputs
See [action.yml](action.yml)
```yaml
- uses: actions/attest-sbom@v1
with:
# Path to the artifact serving as the subject of the attestation. Must
# specify exactly one of "subject-path" or "subject-digest".
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:
# Path to the JSON-formatted SBOM file to attest. When specified, the
# "scan-path" and "sbom-format" inputs are ignored.
sbom-path:
# 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:
# 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/attestaion.jsonl` |
<!-- markdownlint-enable MD013 -->
Attestations are saved in the JSON-serialized [Sigstore bundle][8] 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][9]
format).
## Examples
### Identify Subject and SBOM by Path
For the basic use case, simply add the `attest-sbom` action to your workflow and
supply the path to the artifact and SBOM 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: Generate SBOM
uses: anchore/sbom-action@v0
with:
format: 'spdx-json'
output-file: 'sbom.spdx.json'
- name: Attest
uses: actions/attest-sbom@v1
with:
subject-path: '${{ github.workspace }}/my-app'
sbom-path: 'sbom.spdx.json'
```
### Identify Subjects by Wildcard
If you are generating multiple artifacts, you can generate an attestation for
each by using a wildcard in the `subject-path` input.
```yaml
- uses: actions/attest-sbom@v1
with:
subject-path: 'dist/**/my-bin-*'
sbom-path: '${{ github.workspace }}/my-bin.sbom.spdx.json'
```
For supported wildcards along with behavior and documentation, see
[@actions/glob][10] which is used internally to search for files.
### 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.
> **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/[email protected]
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
format: 'cyclonedx-json'
output-file: 'sbom.cyclonedx.json'
- name: Attest
uses: actions/attest-sbom@v1
id: attest
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
sbom-path: 'sbom.cyclonedx.json'
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
@@ -57,7 +231,7 @@ Documentation for previous versions of this action can be found [here][10].
[6]: https://www.sigstore.dev/
[7]: https://cli.github.com/manual/gh_attestation_verify
[8]:
https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
[9]: https://github.com/actions/attest
[10]:
https://github.com/actions/attest-sbom/tree/v3.0.0?tab=readme-ov-file#actionsattest-sbom
https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto
[9]: https://jsonlines.org/
[10]: https://github.com/actions/toolkit/tree/main/packages/glob#patterns
[11]: https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
+9 -8
View File
@@ -1,12 +1,12 @@
# Release Instructions
Follow the steps below to tag a new release for the `actions/attest-sbom`
action.
Follow the steps below to tag a new release for the
`actions/attest-sbom` action.
If changes were made to the internal `actions/attest-sbom/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.
If changes were made to the internal `actions/attest-sbom/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 `[email protected]` following
@@ -17,8 +17,9 @@ directly to step #5.
git push --tags
```
1. Update the reference to the `actions/attest-sbom/predicate` action in
[`action.yml`](./action.yml) to point to the SHA of the newly created tag.
1. Update the reference to the `actions/attest-sbom/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
+40 -37
View File
@@ -1,38 +1,41 @@
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "./",
"documentNamespace": "https://anchore.com/syft/dir/80b363b6-87f4-4162-853f-60d402537d20",
"creationInfo": {
"licenseListVersion": "3.22",
"creators": ["Organization: Anchore, Inc", "Tool: syft-0.103.1"],
"created": "2024-01-31T18:22:50Z"
},
"packages": [
{
"name": "@ampproject/remapping",
"SPDXID": "SPDXRef-Package-npm--ampproject-remapping-5266573ba4f24a42",
"versionInfo": "2.2.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"sourceInfo": "acquired package info from installed node module manifest file: /yarn.lock",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "Apache-2.0",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceType": "cpe23Type",
"referenceLocator": "cpe:2.3:a:\\@ampproject\\/remapping:\\@ampproject\\/remapping:2.2.1:*:*:*:*:*:*:*"
},
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/%40ampproject/[email protected]"
}
]
}
]
}
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "./",
"documentNamespace": "https://anchore.com/syft/dir/80b363b6-87f4-4162-853f-60d402537d20",
"creationInfo": {
"licenseListVersion": "3.22",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-0.103.1"
],
"created": "2024-01-31T18:22:50Z"
},
"packages": [
{
"name": "@ampproject/remapping",
"SPDXID": "SPDXRef-Package-npm--ampproject-remapping-5266573ba4f24a42",
"versionInfo": "2.2.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"sourceInfo": "acquired package info from installed node module manifest file: /yarn.lock",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "Apache-2.0",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceType": "cpe23Type",
"referenceLocator": "cpe:2.3:a:\\@ampproject\\/remapping:\\@ampproject\\/remapping:2.2.1:*:*:*:*:*:*:*"
},
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/%40ampproject/[email protected]"
}
]
}
]
}
+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()
})
})
+112
View File
@@ -0,0 +1,112 @@
import * as core from '@actions/core'
import * as main from '../src/main'
import * as fs from 'fs'
import os from 'os'
import * as path from 'path'
// Mock the GitHub Actions core library
jest.mock('@actions/core')
const getInputMock = jest.spyOn(core, 'getInput')
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('SBOM Action', () => {
let tempDir = '/'
let outputs = {} as Record<string, string>
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbom'))
jest.resetAllMocks()
setOutputMock.mockImplementation((key, value) => {
outputs[key] = value
})
})
afterEach(() => {
fs.rmdirSync(tempDir, { recursive: true })
outputs = {}
})
it('successfully processes an SBOM', async () => {
const spdxSBOM = JSON.stringify({
spdxVersion: 'SPDX-2.2',
SPDXID: 'SPDXRef-DOCUMENT',
packages: []
})
const filePath = path.join(tempDir, 'spdxSBOM.json')
fs.writeFileSync(filePath, spdxSBOM)
const inputs = {
'sbom-path': filePath
}
getInputMock.mockImplementation(mockInput(inputs))
const originalEnv = process.env
process.env = { ...originalEnv, RUNNER_TEMP: '/tmp' }
// Run the main function
await main.run()
// Verify that outputs were set correctly
expect(setOutputMock).toHaveBeenCalledTimes(2)
expect(setOutputMock).toHaveBeenNthCalledWith(
2,
'predicate-type',
'https://spdx.dev/Document/v2.2'
)
expect(outputs['predicate-path']).toBeTruthy()
const predicatePath = outputs['predicate-path']
// Verify that the temporary file exists
expect(fs.existsSync(predicatePath)).toBe(true)
// Read the content of the temporary file
const fileContent = fs.readFileSync(predicatePath, 'utf-8')
// Verify that the content matches the predicate params
expect(JSON.parse(fileContent)).toEqual(JSON.parse(spdxSBOM))
// Clean up the temporary file
fs.unlinkSync(predicatePath)
process.env = originalEnv
})
it('fails when an error occurs without input', async () => {
await main.run()
expect(setFailedMock).toHaveBeenCalledWith(
'TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string or an instance of Buffer or URL. Received undefined'
)
})
it('fails when an error occurs with wrong sbom format', async () => {
const spdxSBOM = JSON.stringify({
SPDXID: 'SPDXRef-DOCUMENT'
})
const filePath = path.join(tempDir, 'spdxSBOM.json')
fs.writeFileSync(filePath, spdxSBOM)
const inputs = {
'sbom-path': filePath
}
getInputMock.mockImplementation(mockInput(inputs))
const originalEnv = process.env
process.env = { ...originalEnv, RUNNER_TEMP: '/tmp' }
// Run the main function
await main.run()
expect(setFailedMock).toHaveBeenCalledWith('Unsupported SBOM format')
})
})
function mockInput(inputs: Record<string, string>): typeof core.getInput {
return (name: string): string => {
if (name in inputs) {
return inputs[name]
}
return ''
}
}
+125
View File
@@ -0,0 +1,125 @@
import {
storePredicate,
parseSBOMFromPath,
generateSBOMPredicate,
SBOM
} from '../src/sbom'
import type { Predicate } from '@actions/attest'
import * as fs from 'fs'
import os from 'os'
import * as path from 'path'
describe('parseSBOMFromPath', () => {
let tempDir = '/'
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbom'))
})
afterEach(() => {
fs.rmdirSync(tempDir, { recursive: true })
})
it('correctly parses an SPDX file', async () => {
const spdxSBOM = JSON.stringify({
spdxVersion: 'SPDX-2.2',
SPDXID: 'SPDXRef-DOCUMENT'
})
const filePath = path.join(tempDir, 'spdxSBOM.json')
fs.writeFileSync(filePath, spdxSBOM)
await expect(parseSBOMFromPath(filePath)).resolves.toEqual({
type: 'spdx',
object: JSON.parse(spdxSBOM)
})
})
it('correctly parses a CycloneDX file', async () => {
const cycloneDXSBOM = JSON.stringify({
bomFormat: 'CycloneDX',
serialNumber: '123',
specVersion: '1.2'
})
const filePath = path.join(tempDir, 'cyclonedxSBOM.json')
fs.writeFileSync(filePath, cycloneDXSBOM)
await expect(parseSBOMFromPath(filePath)).resolves.toEqual({
type: 'cyclonedx',
object: JSON.parse(cycloneDXSBOM)
})
})
it('throws an error for unsupported SBOM formats', async () => {
const filePath = path.join(tempDir, 'random.json')
fs.writeFileSync(filePath, JSON.stringify({ random: 'value' }))
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
'Unsupported SBOM format'
)
})
})
describe('storePredicate', () => {
it('should store the predicate to a temporary file', () => {
const predicate = { params: { key: 'value' } } as Predicate
// Mocking the process.env['RUNNER_TEMP'] value
const originalEnv = process.env
process.env = { ...originalEnv, RUNNER_TEMP: '/tmp' }
const tempFile = storePredicate(predicate)
// Verify that the temporary file exists
expect(fs.existsSync(tempFile)).toBe(true)
// Read the content of the temporary file
const fileContent = fs.readFileSync(tempFile, 'utf-8')
// Verify that the content matches the predicate params
expect(JSON.parse(fileContent)).toEqual(predicate.params)
// Clean up the temporary file
fs.unlinkSync(tempFile)
// Restore the original process.env
process.env = originalEnv
})
it('should throw an error if RUNNER_TEMP environment variable is missing', () => {
const predicate = { params: { key: 'value' } } as Predicate
// Mocking the process.env['RUNNER_TEMP'] value
const originalEnv = process.env
process.env = {}
// Verify that an error is thrown
expect(() => storePredicate(predicate)).toThrow(
'Missing RUNNER_TEMP environment variable'
)
// Restore the original process.env
process.env = originalEnv
})
})
describe('generateSBOMPredicate', () => {
it('generates SPDX predicate correctly', () => {
const sbom = { type: 'spdx', object: { spdxVersion: 'SPDX-2.2' } } as SBOM
const result = generateSBOMPredicate(sbom)
expect(result.type).toContain('https://spdx.dev/Document/v2.2')
expect(result.params).toEqual(sbom.object)
})
it('generates CycloneDX predicate correctly', () => {
const sbom = { type: 'cyclonedx', object: {} } as SBOM
const result = generateSBOMPredicate(sbom)
expect(result.type).toEqual('https://cyclonedx.org/bom')
expect(result.params).toEqual(sbom.object)
})
it('throws error for unsupported SBOM formats', () => {
const sbom = { type: 'spdx', object: {} }
// @ts-expect-error test error case
expect(() => generateSBOMPredicate(sbom)).toThrow(
'Cannot find spdxVersion in the SBOM'
)
})
})
+18 -38
View File
@@ -9,31 +9,24 @@ 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".
required: false
subject-digest:
description: >
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", "subject-digest", or "subject-checksums".
"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
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.
sbom-path:
description: >
Path to the JSON-formatted SBOM file to attest. File size cannot exceed
16MB.
required: true
Path to the JSON-formatted SBOM file to attest. When specified, the
"scan-path" and "sbom-format" inputs are ignored.
required: false
push-to-registry:
description: >
Whether to push the provenance statement to the image registry. Requires
@@ -41,12 +34,6 @@ inputs:
and that the "subject-digest" parameter be specified. Defaults to false.
default: false
required: false
show-summary:
description: >
Whether to attach a list of generated attestations to the workflow run
summary page. Defaults to true.
default: true
required: false
github-token:
description: >
The GitHub token used to make authenticated API requests.
@@ -55,32 +42,25 @@ 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 }}
runs:
using: 'composite'
steps:
- shell: bash
run: |
echo "::warning::actions/attest-sbom has been deprecated, please use actions/attest instead"
- uses: actions/attest@c32b4b8b198b65d0bd9d63490e847ff7b53989d4 # v4.0.0
- uses: actions/attest-sbom/predicate@534423496eab34674190bc45fdacbb8b1198e07f # [email protected]
id: generate-sbom-predicate
with:
sbom-path: ${{ inputs.sbom-path || steps.sbom-output.outputs.path }}
- uses: actions/attest@bdehamer/skip-attestation-store
id: attest
env:
NODE_OPTIONS: '--max-http-header-size=32768'
with:
subject-path: ${{ inputs.subject-path }}
subject-digest: ${{ inputs.subject-digest }}
subject-name: ${{ inputs.subject-name }}
subject-checksums: ${{ inputs.subject-checksums }}
sbom-path: ${{ inputs.sbom-path }}
predicate-type:
${{ steps.generate-sbom-predicate.outputs.predicate-type }}
predicate-path:
${{ steps.generate-sbom-predicate.outputs.predicate-path }}
push-to-registry: ${{ inputs.push-to-registry }}
show-summary: ${{ inputs.show-summary }}
github-token: ${{ inputs.github-token }}
Generated Vendored
+26988
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+120
View File
@@ -0,0 +1,120 @@
@actions/core
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/http-client
MIT
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@fastify/busboy
MIT
Copyright Brian White. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
tunnel
MIT
The MIT License (MIT)
Copyright (c) 2012 Koichi Kobayashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
undici
MIT
MIT License
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
uuid
MIT
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
process.stdout.write = jest.fn()
+8840
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
{
"name": "actions/attest-sbom",
"description": "Generate signed SBOM attestations",
"version": "1.0.0",
"author": "",
"private": true,
"homepage": "https://github.com/actions/attest-sbom",
"repository": {
"type": "git",
"url": "git+https://github.com/actions/attest-sbom.git"
},
"bugs": {
"url": "https://github.com/actions/attest-sbom/issues"
},
"keywords": [
"actions",
"attestation",
"sbom"
],
"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/core": "^1.10.1"
},
"devDependencies": {
"@actions/attest": "^1.2.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0",
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-jest": "^28.5.0",
"eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"markdownlint-cli": "^0.41.0",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"ts-jest": "^29.1.3",
"typescript": "^5.4.5"
}
}
+19
View File
@@ -0,0 +1,19 @@
name: 'SBOM Predicate'
description: 'Generate predicate for SBOM attestations'
author: 'GitHub'
inputs:
sbom-path:
description: >
Path to the SBOM file to generate sbom statement
required: false
outputs:
predicate-path:
description: >
The path to 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()
+31
View File
@@ -0,0 +1,31 @@
import * as core from '@actions/core'
import {
parseSBOMFromPath,
storePredicate,
generateSBOMPredicate
} from './sbom'
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
try {
const sbomPath = core.getInput('sbom-path')
core.debug(`Reading SBOM from ${sbomPath}`)
const sbom = await parseSBOMFromPath(sbomPath)
// Calculate subject from inputs and generate provenance
const predicate = generateSBOMPredicate(sbom)
const predicatePath = storePredicate(predicate)
core.setOutput('predicate-path', predicatePath)
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)
}
}
+99
View File
@@ -0,0 +1,99 @@
import fs from 'fs'
import * as path from 'path'
import type { Predicate } from '@actions/attest'
export type SBOM = {
type: 'spdx' | 'cyclonedx'
object: object
}
export async function parseSBOMFromPath(filePath: string): Promise<SBOM> {
// Read the file content
const fileContent = await fs.promises.readFile(filePath, 'utf8')
const sbom = JSON.parse(fileContent)
if (checkIsSPDX(sbom)) {
return { type: 'spdx', object: sbom }
} else if (checkIsCycloneDX(sbom)) {
return { type: 'cyclonedx', object: sbom }
}
throw new Error('Unsupported SBOM format')
}
function checkIsSPDX(sbomObject: {
spdxVersion?: string
SPDXID?: string
}): boolean {
if (sbomObject?.spdxVersion && sbomObject?.SPDXID) {
return true
} else {
return false
}
}
function checkIsCycloneDX(sbomObject: {
bomFormat?: string
serialNumber?: string
specVersion?: string
}): boolean {
if (
sbomObject?.bomFormat &&
sbomObject?.serialNumber &&
sbomObject?.specVersion
) {
return true
} else {
return false
}
}
export const storePredicate = (predicate: Predicate): string => {
// random tempfile
const basePath = process.env['RUNNER_TEMP']
if (!basePath) {
throw new Error('Missing RUNNER_TEMP environment variable')
}
const tmpDir = fs.mkdtempSync(path.join(basePath, path.sep))
const tempFile = path.join(tmpDir, 'predicate.json')
// write predicate to file
fs.writeFileSync(tempFile, JSON.stringify(predicate.params))
return tempFile
}
export const generateSBOMPredicate = (sbom: SBOM): Predicate => {
if (sbom.type === 'spdx') {
return generateSPDXIntoto(sbom.object)
}
if (sbom.type === 'cyclonedx') {
return generateCycloneDXIntoto(sbom.object)
}
throw new Error('Unsupported SBOM format')
}
// ref: https://github.com/in-toto/attestation/blob/main/spec/predicates/spdx.md
const generateSPDXIntoto = (sbom: object): Predicate => {
const spdxVersion = (sbom as { spdxVersion?: string })?.['spdxVersion']
if (!spdxVersion) {
throw new Error('Cannot find spdxVersion in the SBOM')
}
const version = spdxVersion.split('-')[1]
return {
type: `https://spdx.dev/Document/v${version}`,
params: sbom
}
}
// ref: https://github.com/in-toto/attestation/blob/main/spec/predicates/cyclonedx.md
const generateCycloneDXIntoto = (sbom: object): Predicate => {
return {
type: 'https://cyclonedx.org/bom',
params: sbom
}
}
+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"]
}