Skip to content

Commit 3b9889d

Browse files
feat(ci): gate dependency risk and publish supply-chain posture
1 parent 2b250ec commit 3b9889d

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

.github/workflows/ci-security-scan.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ on:
1515
default: "false"
1616

1717
jobs:
18+
dependency-review:
19+
name: "New dependency risk"
20+
runs-on: ubuntu-latest
21+
if: inputs.should_skip != 'true' && github.event_name == 'pull_request'
22+
timeout-minutes: 5
23+
permissions:
24+
contents: read
25+
steps:
26+
# Use GitHub's dependency graph instead of rescanning the whole lockfile diff ourselves. This
27+
# blocks newly introduced fixable risk without wedging unrelated PRs on existing findings;
28+
# Renovate and the scheduled image rescans own remediation of the existing baseline.
29+
- name: Review dependency changes
30+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
31+
with:
32+
fail-on-severity: high
33+
license-check: true
34+
vulnerability-check: true
35+
show-openssf-scorecard: true
36+
1837
security-scan:
1938
name: "Dependencies, secrets, and policy"
2039
runs-on: ubuntu-latest

.github/workflows/scorecard.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: OpenSSF Scorecard
2+
3+
# Repository posture changes independently of a code diff. Keep this off the pull-request critical
4+
# path and rescan the default branch weekly; a push catches workflow and policy changes immediately.
5+
on:
6+
schedule:
7+
- cron: "23 4 * * 1"
8+
push:
9+
branches: ["main"]
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions: read-all
16+
17+
jobs:
18+
analysis:
19+
name: Supply-chain posture
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 10
22+
permissions:
23+
contents: read
24+
id-token: write
25+
security-events: write
26+
steps:
27+
- name: Run OpenSSF Scorecard
28+
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
29+
with:
30+
results_file: results.sarif
31+
results_format: sarif
32+
publish_results: true
33+
34+
- name: Upload findings to code scanning
35+
if: always()
36+
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
37+
with:
38+
sarif_file: results.sarif
39+
40+
- name: Preserve the assessment
41+
if: always()
42+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
43+
with:
44+
name: openssf-scorecard-${{ github.sha }}
45+
path: results.sarif
46+
if-no-files-found: error
47+
retention-days: 14

docs/contributor/ci-cd.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,33 @@ commands.
396396

397397
## 📊 CI Features
398398

399+
### Supply-chain feedback
400+
401+
Supply-chain checks run at the point where their result is actionable rather than making every
402+
scanner part of every pull request:
403+
404+
- Pull requests use GitHub's
405+
[dependency review](https://docs.github.com/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review)
406+
to reject newly introduced high or critical vulnerabilities and report dependency-license
407+
changes. This is a diff gate: existing findings remain visible without preventing an unrelated
408+
fix from merging.
409+
- Trivy scans the repository dependency baseline in the required Security leg and publishes SARIF
410+
to code scanning. Image builds additionally evaluate the documented vulnerability policy against
411+
the image that was actually produced; weekly rescans detect vulnerability-database drift after a
412+
clean merge.
413+
- [OpenSSF Scorecard](https://securityscorecards.dev/) evaluates repository and workflow posture on
414+
every push to `main` and weekly (which also catches settings-only drift). It publishes authenticated
415+
results and SARIF without adding another network scanner to the pull-request critical path.
416+
- CodeQL remains enabled through GitHub default setup. GitHub's
417+
[recommended configuration](https://docs.github.com/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning)
418+
owns language discovery and query updates rather than a second workflow duplicating them.
419+
420+
The checks deliberately answer different questions: dependency review prevents a regression,
421+
Trivy inventories current package and image exposure, CodeQL finds source-level data-flow defects,
422+
and Scorecard measures the repository controls that make the resulting artifacts trustworthy.
423+
Release images remain digest-bound, signed, SBOM-attested, provenance-attested, and verified as
424+
described in [Release management](./release-management.mdx#supply-chain-evidence).
425+
399426
### Test Results
400427

401428
All test suites generate JUnit XML reports that are displayed in the **Test Results** tab of each workflow run:

scripts/ci-contract.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,26 @@ void describe("CI contract", () => {
369369
}
370370
});
371371

372+
void test("reviews dependency diffs and measures repository supply-chain posture", async () => {
373+
const security = await readFile(".github/workflows/ci-security-scan.yml", "utf8");
374+
const dependencyReview = job(security, "dependency-review");
375+
assert.match(dependencyReview, /github\.event_name == 'pull_request'/);
376+
assert.match(dependencyReview, /actions\/dependency-review-action@[a-f0-9]{40}/);
377+
assert.match(dependencyReview, /fail-on-severity: high/);
378+
assert.match(dependencyReview, /license-check: true/);
379+
assert.match(dependencyReview, /vulnerability-check: true/);
380+
381+
const scorecard = await readFile(".github/workflows/scorecard.yml", "utf8");
382+
assert.match(scorecard, /^ {2}schedule:/m);
383+
assert.match(scorecard, /^ {2}push:\n {4}branches: \["main"\]/m);
384+
assert.doesNotMatch(scorecard, /^ {2}pull_request:/m);
385+
assert.match(scorecard, /ossf\/scorecard-action@[a-f0-9]{40}/);
386+
assert.match(scorecard, /publish_results: true/);
387+
assert.match(scorecard, /github\/codeql-action\/upload-sarif@[a-f0-9]{40}/);
388+
assert.match(scorecard, /security-events: write/);
389+
assert.match(scorecard, /id-token: write/);
390+
});
391+
372392
void test("pins every external action to a full commit SHA with a version comment", async () => {
373393
const invalid: string[] = [];
374394
const files = await Array.fromAsync(glob(".github/{actions,workflows}/**/*.{yml,yaml}"));

0 commit comments

Comments
 (0)