Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 19 additions & 0 deletions .github/workflows/ci-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ 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
license-check: true
vulnerability-check: true
show-openssf-scorecard: true

security-scan:
name: "Dependencies, secrets, and policy"
runs-on: ubuntu-latest
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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:
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
timeout-minutes: 10
permissions:
contents: read
id-token: write
security-events: write
steps:
- 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
if: always()
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: results.sarif

- name: Preserve the assessment
if: always()
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
27 changes: 27 additions & 0 deletions docs/contributor/ci-cd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,33 @@ 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. 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` and weekly (which also catches settings-only drift). 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
20 changes: 20 additions & 0 deletions scripts/ci-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,26 @@ 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, /license-check: true/);
assert.match(dependencyReview, /vulnerability-check: true/);

const scorecard = await readFile(".github/workflows/scorecard.yml", "utf8");
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, /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