perpare v4 release (#253)

Signed-off-by: Brian DeHamer <bdehamer@github.com>
This commit is contained in:
Brian DeHamer
2026-02-25 15:03:50 -08:00
committed by GitHub
parent b74e95116c
commit 07e74fc4e7
24 changed files with 22 additions and 38634 deletions

View File

@@ -11,18 +11,3 @@ updates:
- patch
ignore:
- 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

View File

@@ -1,66 +0,0 @@
# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dist
path: dist/

View File

@@ -12,40 +12,6 @@ on:
permissions: {}
jobs:
test-typescript:
name: TypeScript Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
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
runs-on: ubuntu-latest

View File

@@ -1,50 +0,0 @@
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
id: initialize
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
source-root: src
- name: Autobuild
id: autobuild
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

View File

@@ -1 +0,0 @@
24.5.0

View File

@@ -1,3 +0,0 @@
dist/
node_modules/
coverage/

View File

@@ -1,16 +0,0 @@
{
"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"
}

294
README.md
View File

@@ -1,5 +1,12 @@
# `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.
@@ -21,8 +28,8 @@ initiated.
Attestations can be verified using the [`attestation` command in the GitHub
CLI][7].
See [Using artifact attestations to establish provenance for builds][11] for
more information on artifact attestations.
See [Using artifact attestations to establish provenance for builds][8] for more
information on artifact attestations.
<!-- prettier-ignore-start -->
> [!NOTE]
@@ -36,281 +43,12 @@ more information on artifact attestations.
## Usage
Within the GitHub Actions workflow which builds some artifact you would like to
attest:
**As of version 4, `actions/attest-sbom` is simply a wrapper on top of
[`actions/attest`][9].**
1. Ensure that the following permissions are set:
Please see the [`actions/attest`][9] repository for usage information.
```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@v3
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@v3
with:
# 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).
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", "subject-digest", or "subject-checksums".
subject-digest:
# Subject name as it should appear in the attestation. Required when
# identifying the subject with the "subject-digest" input.
subject-name:
# Path to checksums file containing digest and name of subjects for
# attestation. Must specify exactly one of "subject-path", "subject-digest",
# or "subject-checksums".
subject-checksums:
# Path to the JSON-formatted SBOM file to attest. File size cannot exceed
# 16MB.
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:
# 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 |
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| `attestation-id` | GitHub ID for the attestation | `123456` |
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
<!-- markdownlint-enable MD013 -->
Attestations are saved in the JSON-serialized [Sigstore bundle][8] format.
If multiple subjects are being attested at the same time, a single attestation
will be created with references to each of the supplied subjects.
The absolute path to the generated attestation is appended to the file
`${RUNNER_TEMP}/created_attestation_paths.txt`. This file will accumulate the
paths to all attestations created over the course of a single workflow.
## Attestation Limits
### Subject Limits
No more than 1024 subjects can be attested at the same time.
### SBOM Limits
The SBOM supplied via the `sbom-path` input cannot exceed 16MB.
## 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:
runs-on: ubuntu-latest
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@v3
with:
subject-path: '${{ github.workspace }}/my-app'
sbom-path: 'sbom.spdx.json'
```
### Identify Multiple Subjects
If you are generating multiple artifacts, you can attest all of them at the same
time by using a wildcard in the `subject-path` input.
```yaml
- uses: actions/attest-sbom@v3
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.
Alternatively, you can explicitly list multiple subjects with either a comma or
newline delimited list:
```yaml
- uses: actions/attest-sbom@v3
with:
subject-path: 'dist/foo, dist/bar'
```
```yaml
- uses: actions/attest-sbom@v3
with:
subject-path: |
dist/foo
dist/bar
```
### Identify Subjects with Checksums File
If you are using tools like
[goreleaser](https://goreleaser.com/customization/checksum/) or
[jreleaser](https://jreleaser.org/guide/latest/reference/checksum.html) which
generate a checksums file you can identify the attestation subjects by passing
the path of the checksums file to the `subject-checksums` input. Each of the
artifacts identified in the checksums file will be listed as a subject for the
attestation.
```yaml
- name: Calculate artifact digests
run: |
shasum -a 256 foo_0.0.1_* > subject.checksums.txt
- uses: actions/attest-sbom@v3
with:
subject-checksums: subject.checksums.txt
sbom-path: sbom.spdx.json
```
<!-- markdownlint-disable MD038 -->
The file referenced by the `subject-checksums` input must conform to the same
format used by the shasum tools. Each subject should be listed on a separate
line including the hex-encoded digest (either SHA256 or SHA512), a space, a
single character flag indicating either binary (`*`) or text (` `) input mode,
and the filename.
<!-- markdownlint-enable MD038 -->
```text
b569bf992b287f55d78bf8ee476497e9b7e9d2bf1c338860bfb905016218c740 foo_0.0.1_darwin_amd64
a54fc515e616cac7fcf11a49d5c5ec9ec315948a5935c1e11dd610b834b14dde foo_0.0.1_darwin_arm64
```
### 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/build-push-action@v5.0.0
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@v3
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
```
Documentation for previous versions of this action can be found [here][10].
[1]: https://github.com/actions/toolkit/tree/main/packages/attest
[2]: https://github.com/in-toto/attestation/tree/main/spec/v1
@@ -319,7 +57,7 @@ jobs:
[6]: https://www.sigstore.dev/
[7]: https://cli.github.com/manual/gh_attestation_verify
[8]:
https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto
[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]: https://github.com/actions/attest
[10]:
https://github.com/actions/attest-sbom/tree/v3.0.0?tab=readme-ov-file#actionsattest-sbom

View File

@@ -1,17 +0,0 @@
/**
* 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', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../src/index')
expect(runMock).toHaveBeenCalled()
})
})

View File

@@ -1,112 +0,0 @@
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.rmSync(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 ''
}
}

View File

@@ -1,130 +0,0 @@
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.rmSync(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('throws an error for missing SPDX version', () => {
const sbom = { type: 'spdx' } as SBOM
expect(() => generateSBOMPredicate(sbom)).toThrow(
'Cannot find spdxVersion in the SBOM'
)
})
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: 'foo', object: {} }
// @ts-expect-error test error case
expect(() => generateSBOMPredicate(sbom)).toThrow('Unsupported SBOM format')
})
})

View File

@@ -67,11 +67,11 @@ outputs:
runs:
using: 'composite'
steps:
- uses: actions/attest-sbom/predicate@55e972012fb8695c1b0049174547f1fcb4baa8a5 # predicate@2.0.0
id: generate-sbom-predicate
with:
sbom-path: ${{ inputs.sbom-path }}
- uses: actions/attest@e59cbc1ad1ac2d59339667419eb8cdde6eb61e3d # v3.2.0
- shell: bash
run: |
echo "::warning::actions/attest-sbom has been deprecated, please use actions/attest instead"
- uses: actions/attest@c32b4b8b198b65d0bd9d63490e847ff7b53989d4 # v4.0.0
id: attest
env:
NODE_OPTIONS: '--max-http-header-size=32768'
@@ -80,10 +80,7 @@ runs:
subject-digest: ${{ inputs.subject-digest }}
subject-name: ${{ inputs.subject-name }}
subject-checksums: ${{ inputs.subject-checksums }}
predicate-type:
${{ steps.generate-sbom-predicate.outputs.predicate-type }}
predicate-path:
${{ steps.generate-sbom-predicate.outputs.predicate-path }}
sbom-path: ${{ inputs.sbom-path }}
push-to-registry: ${{ inputs.push-to-registry }}
show-summary: ${{ inputs.show-summary }}
github-token: ${{ inputs.github-token }}

28011
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

131
dist/licenses.txt generated vendored
View File

@@ -1,131 +0,0 @@
@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/exec
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.
@actions/io
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.
@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.

View File

@@ -1,92 +0,0 @@
import eslint from '@eslint/js'
import importplugin from 'eslint-plugin-import'
import jestplugin from 'eslint-plugin-jest'
import tseslint from 'typescript-eslint'
export default tseslint.config(
// Ignore non-project files
{
name: 'ignore',
ignores: ['.github', 'dist', 'coverage', '**/*.json', 'jest.setup.js', 'eslint.config.mjs']
},
// Use recommended rules from ESLint, TypeScript, and other plugins
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
jestplugin.configs['flat/recommended'],
importplugin.flatConfigs.recommended,
importplugin.flatConfigs.typescript,
// Override some rules
{
name: 'project-settings',
languageOptions: {
ecmaVersion: 2023,
parserOptions: {
project: ['./tsconfig.lint.json']
}
},
rules: {
// eslint rules
eqeqeq: ['error', 'smart'],
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': 'off',
'no-implicit-globals': 'error',
'no-inner-declarations': 'error',
'no-invalid-this': 'error',
'no-return-assign': 'error',
'no-sequences': 'error',
'no-shadow': 'error',
'no-useless-concat': 'error',
'object-shorthand': ['error', 'always', { avoidQuotes: true }],
'one-var': ['error', 'never'],
'prefer-template': 'error',
// typescript-eslint rules
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/explicit-function-return-type': [
'error',
{ allowExpressions: true }
],
'@typescript-eslint/explicit-member-accessibility': [
'error',
{ accessibility: 'no-public' }
],
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-useless-constructor': '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-template-expressions': 'off',
// eslint-plugin-import rules
'import/extensions': 'error',
'import/first': 'error',
'import/no-absolute-path': 'error',
'import/no-commonjs': 'error',
'import/no-deprecated': 'warn',
'import/no-dynamic-require': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/no-namespace': 'off',
'import/no-unresolved': ['error', { ignore: ['csv-parse/sync'] }],
'import/no-anonymous-default-export': [
'error',
{
allowAnonymousClass: false,
allowAnonymousFunction: false,
allowArray: true,
allowArrowFunction: false,
allowLiteral: true,
allowObject: true
}
]
}
}
)

View File

@@ -1 +0,0 @@
process.stdout.write = jest.fn()

9391
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
{
"name": "actions/attest-sbom",
"description": "Generate signed SBOM attestations",
"version": "2.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": ">=24"
},
"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",
"lint:markdown": "npx markdownlint --config .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": "^2.0.2"
},
"devDependencies": {
"@actions/attest": "^2.2.0",
"@eslint/js": "^9.39.2",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.9",
"@vercel/ncc": "^0.38.4",
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.12.1",
"jest": "^30.2.0",
"markdownlint-cli": "^0.47.0",
"prettier": "^3.8.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.0"
}
}

View File

@@ -1,19 +0,0 @@
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: node24
main: ../dist/index.js

View File

@@ -1,7 +0,0 @@
/**
* The entrypoint for the action.
*/
import { run } from './main'
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run()

View File

@@ -1,31 +0,0 @@
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)
}
}

View File

@@ -1,100 +0,0 @@
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) as object
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 => {
switch (sbom.type) {
case 'spdx':
return generateSPDXIntoto(sbom.object)
case 'cyclonedx':
return generateCycloneDXIntoto(sbom.object)
default:
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
}
}

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"rootDir": "./src",
"moduleResolution": "NodeNext",
"isolatedModules": true,
"baseUrl": "./",
"sourceMap": true,
"outDir": "./dist",
"noImplicitAny": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"newLine": "lf"
},
"exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
}

View File

@@ -1,9 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./__tests__/**/*", "./src/**/*"],
"exclude": ["./dist", "./node_modules", "./coverage", "*.json"]
}