Skip to content

Commit 1aad74d

Browse files
authored
Rewrite GitHub Action for Marketplace and document new features (#8)
Convert action.yml from Docker to composite action that downloads pre-built binaries from releases. Add fail-on input, vuln-count output, proper GITHUB_OUTPUT writes, and default vex-triage to true. Update README with --fail-on flag documentation, severity classification, human-readable summary example, and refreshed GitHub Action section with inputs/outputs tables. Closes #7
1 parent f392f18 commit 1aad74d

2 files changed

Lines changed: 201 additions & 41 deletions

File tree

README.md

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Built for EU Cyber Resilience Act (CRA) compliance. forgeseal's own releases are
1111
| **SBOM Generation** | CycloneDX v1.4/v1.5/v1.6 from any JS/TS lockfile (JSON and XML output) |
1212
| **Sigstore Signing** | Keyless signing via Fulcio + Rekor transparency log (OIDC identity) |
1313
| **SLSA Provenance** | In-toto attestations with SLSA v1 provenance predicate |
14-
| **VEX Management** | OpenVEX v0.2 document CRUD, automated triage via OSV.dev |
14+
| **VEX Management** | OpenVEX v0.2 document CRUD, automated triage via OSV.dev, CVSS severity classification |
1515
| **Verification** | Validate signatures, bundles, and attestation integrity |
1616
| **Pipeline** | Single command: SBOM, sign, attest, triage |
1717

@@ -154,7 +154,7 @@ forgeseal vex triage --sbom sbom.cdx.json --format cyclonedx -o sbom-with-vex.js
154154

155155
**Justifications** (for `not_affected`): `component_not_present`, `vulnerable_code_not_present`, `vulnerable_code_cannot_be_controlled_by_adversary`, `vulnerable_code_not_in_execute_path`, `inline_mitigations_already_exist`
156156

157-
The `triage` subcommand queries the [OSV.dev](https://osv.dev) batch API, batching PURLs in groups of 1000, and generates `under_investigation` stubs for each discovered vulnerability.
157+
The `triage` subcommand queries the [OSV.dev](https://osv.dev) batch API, batching PURLs in groups of 1000, classifies each vulnerability by CVSS severity (critical, high, medium, low), and generates `under_investigation` stubs for each discovered vulnerability. Use `--fail-on <severity>` to fail the command when vulnerabilities at or above the threshold are found.
158158

159159
### `forgeseal verify`
160160

@@ -190,17 +190,44 @@ forgeseal pipeline --dir . --vex-triage --identity-token $TOKEN
190190
| `--sign` | `true` | Sign artifacts with Sigstore |
191191
| `--attest` | `true` | Generate SLSA provenance |
192192
| `--vex-triage` | `false` | Run VEX triage against OSV.dev |
193+
| `--fail-on` | (none) | Fail if vulnerabilities at or above severity: `critical`, `high`, `medium`, `low` |
193194
| `--include-dev` | `false` | Include devDependencies |
194195
| `--identity-token` | (auto) | Explicit OIDC token |
195196

197+
Setting `--fail-on` automatically enables VEX triage.
198+
196199
**Pipeline steps:**
197200
1. Parse lockfile, generate CycloneDX SBOM (`sbom.cdx.json`)
198201
2. Sign SBOM (`sbom.cdx.json.sigstore.json`)
199202
3. Generate SLSA provenance attestation (`sbom.cdx.json.intoto.jsonl`), optionally sign it
200203
4. Query OSV.dev and generate VEX document (`vex.json`)
201204

205+
**Vulnerability summary:** When VEX triage runs, forgeseal prints a severity breakdown to stderr:
206+
207+
```
208+
🔴 CRITICAL 2 CRITICAL 🟡 HIGH 5 HIGH 🟠 MEDIUM 12 MEDIUM ⚪ LOW 3 LOW
209+
210+
CRITICAL:
211+
lodash@4.17.20 Prototype Pollution CVE-2021-23337
212+
express@4.17.1 Path Traversal CVE-2024-29041
213+
214+
Scanned 156 components, found 42 vulnerabilities
215+
```
216+
217+
**CI gating:** Use `--fail-on` to block builds when vulnerabilities exceed a threshold:
218+
219+
```bash
220+
# Fail if any critical or high vulnerabilities found
221+
forgeseal pipeline --dir . --fail-on high
222+
223+
# Fail only on critical
224+
forgeseal pipeline --dir . --fail-on critical
225+
```
226+
202227
## GitHub Action
203228

229+
Available on the [GitHub Marketplace](https://github.com/marketplace/actions/forgeseal).
230+
204231
```yaml
205232
name: Supply Chain Security
206233
on: [push]
@@ -215,22 +242,43 @@ jobs:
215242
steps:
216243
- uses: actions/checkout@v4
217244

218-
- uses: sn45/forgeseal@v1
245+
- uses: sns45/forgeseal@v1
246+
id: forgeseal
219247
with:
220-
command: pipeline
221248
dir: '.'
222-
output-dir: './forgeseal-output'
223-
sign: 'true'
224-
attest: 'true'
225-
vex-triage: 'true'
249+
fail-on: high # Fail CI if high or critical vulnerabilities found
226250

227251
- uses: actions/upload-artifact@v4
228252
with:
229253
name: supply-chain-artifacts
230254
path: ./forgeseal-output/
231255
```
232256
233-
The action runs in a Docker container and automatically obtains OIDC tokens from the GitHub Actions runtime.
257+
### Action Inputs
258+
259+
| Input | Default | Description |
260+
|---|---|---|
261+
| `command` | `pipeline` | Command to run: `pipeline`, `sbom`, `sign`, `attest`, `vex` |
262+
| `dir` | `.` | Project directory |
263+
| `output-dir` | `./forgeseal-output` | Output directory for artifacts |
264+
| `sign` | `true` | Sign artifacts with Sigstore keyless signing |
265+
| `attest` | `true` | Generate SLSA v1 provenance attestation |
266+
| `vex-triage` | `true` | Run VEX vulnerability triage against OSV.dev |
267+
| `fail-on` | (none) | Fail if vulnerabilities at or above severity: `critical`, `high`, `medium`, `low` |
268+
| `include-dev` | `false` | Include devDependencies in SBOM |
269+
| `version` | `latest` | forgeseal version to install |
270+
271+
### Action Outputs
272+
273+
| Output | Description |
274+
|---|---|
275+
| `sbom-path` | Path to the generated CycloneDX SBOM |
276+
| `bundle-path` | Path to the Sigstore signature bundle |
277+
| `attestation-path` | Path to the SLSA provenance attestation |
278+
| `vex-path` | Path to the VEX document |
279+
| `vuln-count` | Number of vulnerabilities found by VEX triage |
280+
281+
The action downloads a pre-built binary from GitHub Releases (no Docker build overhead). OIDC tokens are obtained automatically when the workflow has `permissions: id-token: write`.
234282

235283
## Configuration
236284

@@ -279,7 +327,7 @@ internal/
279327
280328
**Signing abstraction.** The `Signer` interface provides `SignBlob` (raw content) and `SignDSSE` (in-toto envelope). The current implementation generates ephemeral ECDSA P-256 signatures. Full Sigstore integration (Fulcio certificate issuance + Rekor log recording) can be added by depending on `sigstore-go` without changing the interface.
281329
282-
**VEX triage.** PURLs extracted from the SBOM are batched in groups of 1000 and sent to the OSV.dev `/v1/querybatch` endpoint. Results are mapped to `under_investigation` VEX stubs for manual review.
330+
**VEX triage.** PURLs extracted from the SBOM are batched in groups of 1000 and sent to the OSV.dev `/v1/querybatch` endpoint. Each vulnerability is classified by CVSS v3 severity (critical 9.0+, high 7.0+, medium 4.0+, low 0.1+). Results are mapped to VEX stubs with severity metadata. The `--fail-on` flag enables CI gating by exiting non-zero when vulnerabilities meet or exceed the specified threshold.
283331
284332
## Output Artifacts
285333

action.yml

Lines changed: 143 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
name: 'forgeseal'
2-
description: 'Supply chain security for JS/TS projects: SBOM, Sigstore signing, SLSA provenance, VEX'
2+
description: 'Supply chain security for JS/TS: SBOM generation, Sigstore signing, SLSA provenance, VEX triage'
3+
author: 'sns45'
4+
35
branding:
46
icon: 'shield'
57
color: 'blue'
68

79
inputs:
810
command:
9-
description: 'Command to run: sbom, sign, attest, vex, pipeline'
10-
required: true
11-
default: 'pipeline'
12-
lockfile:
13-
description: 'Explicit path to lockfile'
11+
description: 'Command to run: pipeline, sbom, sign, attest, vex'
1412
required: false
13+
default: 'pipeline'
1514
dir:
1615
description: 'Project directory'
1716
required: false
@@ -20,52 +19,165 @@ inputs:
2019
description: 'Output directory for artifacts'
2120
required: false
2221
default: './forgeseal-output'
23-
include-dev:
24-
description: 'Include devDependencies in SBOM'
22+
lockfile:
23+
description: 'Explicit path to lockfile (auto-detected if omitted)'
2524
required: false
26-
default: 'false'
2725
sign:
28-
description: 'Sign artifacts with Sigstore'
26+
description: 'Sign artifacts with Sigstore keyless signing'
2927
required: false
3028
default: 'true'
3129
attest:
32-
description: 'Generate SLSA provenance attestation'
30+
description: 'Generate SLSA v1 provenance attestation'
3331
required: false
3432
default: 'true'
3533
vex-triage:
36-
description: 'Run VEX triage against OSV.dev'
34+
description: 'Run VEX vulnerability triage against OSV.dev'
35+
required: false
36+
default: 'true'
37+
fail-on:
38+
description: 'Fail if vulnerabilities at or above this severity: critical, high, medium, low'
39+
required: false
40+
include-dev:
41+
description: 'Include devDependencies in SBOM'
3742
required: false
3843
default: 'false'
3944
upload-assets:
40-
description: 'Upload artifacts to GitHub Release'
45+
description: 'Upload artifacts to the GitHub Release (requires release context)'
4146
required: false
4247
default: 'false'
4348
version:
44-
description: 'forgeseal version to use'
49+
description: 'forgeseal version to install (e.g. 0.1.0). Defaults to latest release.'
4550
required: false
4651
default: 'latest'
4752

4853
outputs:
4954
sbom-path:
50-
description: 'Path to generated SBOM'
55+
description: 'Path to the generated CycloneDX SBOM'
56+
value: ${{ steps.run.outputs.sbom-path }}
5157
bundle-path:
52-
description: 'Path to Sigstore bundle'
58+
description: 'Path to the Sigstore signature bundle'
59+
value: ${{ steps.run.outputs.bundle-path }}
5360
attestation-path:
54-
description: 'Path to SLSA attestation'
61+
description: 'Path to the SLSA provenance attestation'
62+
value: ${{ steps.run.outputs.attestation-path }}
5563
vex-path:
56-
description: 'Path to VEX document'
64+
description: 'Path to the VEX document'
65+
value: ${{ steps.run.outputs.vex-path }}
66+
vuln-count:
67+
description: 'Number of vulnerabilities found by VEX triage'
68+
value: ${{ steps.run.outputs.vuln-count }}
5769

5870
runs:
59-
using: 'docker'
60-
image: 'Dockerfile'
61-
args:
62-
- ${{ inputs.command }}
63-
- '--dir'
64-
- ${{ inputs.dir }}
65-
- '--output-dir'
66-
- ${{ inputs.output-dir }}
67-
env:
68-
FORGESEAL_INCLUDE_DEV: ${{ inputs.include-dev }}
69-
FORGESEAL_SIGN: ${{ inputs.sign }}
70-
FORGESEAL_ATTEST: ${{ inputs.attest }}
71-
FORGESEAL_VEX_TRIAGE: ${{ inputs.vex-triage }}
71+
using: 'composite'
72+
steps:
73+
- name: Install forgeseal
74+
id: install
75+
shell: bash
76+
run: |
77+
set -euo pipefail
78+
79+
VERSION="${{ inputs.version }}"
80+
REPO="sns45/forgeseal"
81+
82+
if [ "$VERSION" = "latest" ]; then
83+
VERSION=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name' 2>/dev/null || true)
84+
if [ -z "$VERSION" ]; then
85+
VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | cut -d'"' -f4)
86+
fi
87+
fi
88+
89+
# Strip leading v
90+
VERSION="${VERSION#v}"
91+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
92+
93+
OS="linux"
94+
ARCH="$(uname -m)"
95+
case "$ARCH" in
96+
x86_64) ARCH="amd64" ;;
97+
aarch64) ARCH="arm64" ;;
98+
esac
99+
100+
TARBALL="forgeseal_${VERSION}_${OS}_${ARCH}.tar.gz"
101+
URL="https://github.com/${REPO}/releases/download/v${VERSION}/${TARBALL}"
102+
103+
echo "Downloading forgeseal v${VERSION} (${OS}/${ARCH})..."
104+
curl -fsSL "$URL" -o "/tmp/${TARBALL}"
105+
tar -xzf "/tmp/${TARBALL}" -C /tmp
106+
chmod +x /tmp/forgeseal
107+
sudo mv /tmp/forgeseal /usr/local/bin/forgeseal
108+
109+
echo "Installed forgeseal v${VERSION}"
110+
forgeseal version
111+
112+
- name: Run forgeseal
113+
id: run
114+
shell: bash
115+
run: |
116+
set -euo pipefail
117+
118+
COMMAND="${{ inputs.command }}"
119+
DIR="${{ inputs.dir }}"
120+
OUTPUT_DIR="${{ inputs.output-dir }}"
121+
122+
# Build argument list
123+
ARGS=("$COMMAND")
124+
125+
if [ "$COMMAND" = "pipeline" ]; then
126+
ARGS+=("--dir" "$DIR" "--output-dir" "$OUTPUT_DIR")
127+
128+
if [ "${{ inputs.sign }}" = "false" ]; then
129+
ARGS+=("--sign=false")
130+
fi
131+
if [ "${{ inputs.attest }}" = "false" ]; then
132+
ARGS+=("--attest=false")
133+
fi
134+
if [ "${{ inputs.vex-triage }}" = "true" ]; then
135+
ARGS+=("--vex-triage")
136+
fi
137+
if [ -n "${{ inputs.fail-on }}" ]; then
138+
ARGS+=("--fail-on" "${{ inputs.fail-on }}")
139+
fi
140+
if [ "${{ inputs.include-dev }}" = "true" ]; then
141+
ARGS+=("--include-dev")
142+
fi
143+
if [ -n "${{ inputs.lockfile }}" ]; then
144+
ARGS+=("--lockfile" "${{ inputs.lockfile }}")
145+
fi
146+
fi
147+
148+
echo "Running: forgeseal ${ARGS[*]}"
149+
forgeseal "${ARGS[@]}"
150+
151+
# Set outputs
152+
if [ -f "${OUTPUT_DIR}/sbom.cdx.json" ]; then
153+
echo "sbom-path=${OUTPUT_DIR}/sbom.cdx.json" >> "$GITHUB_OUTPUT"
154+
fi
155+
if [ -f "${OUTPUT_DIR}/sbom.cdx.json.sigstore.json" ]; then
156+
echo "bundle-path=${OUTPUT_DIR}/sbom.cdx.json.sigstore.json" >> "$GITHUB_OUTPUT"
157+
fi
158+
if [ -f "${OUTPUT_DIR}/sbom.cdx.json.intoto.jsonl" ]; then
159+
echo "attestation-path=${OUTPUT_DIR}/sbom.cdx.json.intoto.jsonl" >> "$GITHUB_OUTPUT"
160+
fi
161+
if [ -f "${OUTPUT_DIR}/vex.json" ]; then
162+
echo "vex-path=${OUTPUT_DIR}/vex.json" >> "$GITHUB_OUTPUT"
163+
# Extract vuln count from VEX document
164+
VULN_COUNT=$(jq '.statements | length' "${OUTPUT_DIR}/vex.json" 2>/dev/null || echo "0")
165+
echo "vuln-count=${VULN_COUNT}" >> "$GITHUB_OUTPUT"
166+
fi
167+
168+
- name: Upload artifacts to release
169+
if: inputs.upload-assets == 'true' && github.event_name == 'release'
170+
shell: bash
171+
run: |
172+
set -euo pipefail
173+
OUTPUT_DIR="${{ inputs.output-dir }}"
174+
TAG="${{ github.event.release.tag_name }}"
175+
176+
for file in "${OUTPUT_DIR}"/*; do
177+
if [ -f "$file" ]; then
178+
echo "Uploading $(basename "$file") to release ${TAG}..."
179+
gh release upload "$TAG" "$file" --clobber
180+
fi
181+
done
182+
env:
183+
GH_TOKEN: ${{ github.token }}

0 commit comments

Comments
 (0)