Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ on:
default: "false"

jobs:
dependency-review:
name: "New dependency risk"
runs-on: ubuntu-latest
if: inputs.should_skip != 'true' && github.event_name == 'pull_request'
timeout-minutes: 5
permissions:
contents: read
steps:
# Use GitHub's dependency graph instead of rescanning the whole lockfile diff ourselves. This
# blocks newly introduced fixable risk without wedging unrelated PRs on existing findings;
# Renovate and the scheduled image rescans own remediation of the existing baseline.
- name: Review dependency changes
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
# Build and test tools execute in CI and can compromise release artifacts; unknown scope
# is not evidence that a dependency is harmless.
fail-on-scopes: runtime, development, unknown
license-check: true
show-patched-versions: true
vulnerability-check: true

security-scan:
name: "Dependencies, secrets, and policy"
runs-on: ubuntu-latest
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: OpenSSF Scorecard

# Repository posture changes independently of a code diff. Keep this off the pull-request critical
# path and rescan the default branch weekly; a push catches workflow and policy changes immediately.
on:
# Scorecard's Branch-Protection check changes when the ruleset changes, not when source changes.
branch_protection_rule:
schedule:
- cron: "23 4 * * 1"
push:
branches: ["main"]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: read-all

jobs:
analysis:
name: Supply-chain posture
runs-on: ubuntu-latest
# Publishing is supported only from the default branch. This also prevents a renamed default
# branch from silently running the push trigger with an identity Scorecard will not publish.
if: github.event.repository.default_branch == github.ref_name
timeout-minutes: 10
permissions:
contents: read
id-token: write
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Run OpenSSF Scorecard
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload findings to code scanning
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: results.sarif

- name: Preserve the assessment
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: openssf-scorecard-${{ github.sha }}
path: results.sarif
if-no-files-found: error
retention-days: 14
28 changes: 28 additions & 0 deletions docs/contributor/ci-cd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,34 @@ commands.

## 📊 CI Features

### Supply-chain feedback

Supply-chain checks run at the point where their result is actionable rather than making every
scanner part of every pull request:

- Pull requests use GitHub's
[dependency review](https://docs.github.com/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review)
to reject newly introduced high or critical vulnerabilities and report dependency-license
changes. Runtime, development, and unknown-scope dependencies are all covered because build tools
execute inside the release trust boundary. This is a diff gate: existing findings remain visible
without preventing an unrelated fix from merging.
- Trivy scans the repository dependency baseline in the required Security leg and publishes SARIF
to code scanning. Image builds additionally evaluate the documented vulnerability policy against
the image that was actually produced; weekly rescans detect vulnerability-database drift after a
clean merge.
- [OpenSSF Scorecard](https://securityscorecards.dev/) evaluates repository and workflow posture on
every push to `main`, after branch-protection changes, and weekly. It publishes authenticated
results and SARIF without adding another network scanner to the pull-request critical path.
- CodeQL remains enabled through GitHub default setup. GitHub's
[recommended configuration](https://docs.github.com/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning)
owns language discovery and query updates rather than a second workflow duplicating them.

The checks deliberately answer different questions: dependency review prevents a regression,
Trivy inventories current package and image exposure, CodeQL finds source-level data-flow defects,
and Scorecard measures the repository controls that make the resulting artifacts trustworthy.
Release images remain digest-bound, signed, SBOM-attested, provenance-attested, and verified as
described in [Release management](./release-management.mdx#supply-chain-evidence).

### Test Results

All test suites generate JUnit XML reports that are displayed in the **Test Results** tab of each workflow run:
Expand Down
26 changes: 26 additions & 0 deletions scripts/ci-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,32 @@ void describe("CI contract", () => {
}
});

void test("reviews dependency diffs and measures repository supply-chain posture", async () => {
const security = await readFile(".github/workflows/ci-security-scan.yml", "utf8");
const dependencyReview = job(security, "dependency-review");
assert.match(dependencyReview, /github\.event_name == 'pull_request'/);
assert.match(dependencyReview, /actions\/dependency-review-action@[a-f0-9]{40}/);
assert.match(dependencyReview, /fail-on-severity: high/);
assert.match(dependencyReview, /fail-on-scopes: runtime, development, unknown/);
assert.match(dependencyReview, /license-check: true/);
assert.match(dependencyReview, /show-patched-versions: true/);
assert.match(dependencyReview, /vulnerability-check: true/);

const scorecard = await readFile(".github/workflows/scorecard.yml", "utf8");
assert.match(scorecard, /^ {2}branch_protection_rule:/m);
assert.match(scorecard, /^ {2}schedule:/m);
assert.match(scorecard, /^ {2}push:\n {4}branches: \["main"\]/m);
assert.doesNotMatch(scorecard, /^ {2}pull_request:/m);
assert.match(scorecard, /if: github\.event\.repository\.default_branch == github\.ref_name/);
assert.match(scorecard, /actions\/checkout@[a-f0-9]{40}/);
assert.match(scorecard, /persist-credentials: false/);
assert.match(scorecard, /ossf\/scorecard-action@[a-f0-9]{40}/);
assert.match(scorecard, /publish_results: true/);
assert.match(scorecard, /github\/codeql-action\/upload-sarif@[a-f0-9]{40}/);
assert.match(scorecard, /security-events: write/);
assert.match(scorecard, /id-token: write/);
});

void test("pins every external action to a full commit SHA with a version comment", async () => {
const invalid: string[] = [];
const files = await Array.fromAsync(glob(".github/{actions,workflows}/**/*.{yml,yaml}"));
Expand Down
Loading