Skip to content

Security and Supply Chain

Ankit Upadhyay edited this page Aug 13, 2026 · 1 revision

Security and supply chain

This page describes the automated security controls that run against every change to openrunic, and how to reproduce each one locally. It is for contributors whose pull request is blocked by a scanner and for anyone evaluating the project's security posture.

For reporting a vulnerability, see Security policy. For how authorization works inside the application, see Security model.

What runs on every pull request

Control Tool Workflow Gate
Static analysis CodeQL .github/workflows/codeql.yml Findings appear in the Security tab
Secret detection Gitleaks 8.24.2 .github/workflows/secret-scan.yml Fails on any finding in the commit range
Secret detection, local secretlint .husky/pre-commit via lint-staged Fails the commit
New advisories dependency-review-action .github/workflows/dependency-review.yml Fails at moderate severity or above
SBOM syft 1.51.0 .github/workflows/supply-chain.yml Produces SPDX and CycloneDX
Vulnerabilities grype 0.117.0 .github/workflows/supply-chain.yml Fails on critical
Licence policy grant 0.6.8 .github/workflows/supply-chain.yml Deny by default against the allow list
Repository posture OpenSSF Scorecard .github/workflows/scorecard.yml Published to the public API

CodeQL

Two languages are analysed: javascript-typescript and actions. Both use build-mode: none, so the database is extracted straight from source and no dependency install is needed.

The scan scope lives in .github/codeql/codeql-config.yml. It currently has an empty paths-ignore, because nothing in the repository is generated or vendored. Add exclusions there as generated asset trees appear, rather than letting their findings drown the real ones.

Secret scanning

Two layers, deliberately different.

Locally, lint-staged.config.mjs runs secretlint over a wide file set including config, shell, env-shaped, TOML, XML, and plist files that Prettier never touches:

pnpm check:secrets     # secretlint --no-gitignore "**/*"

In CI, Gitleaks scans the commit range: base..head on a pull request, before..sha on a push. The Gitleaks binary is downloaded from a pinned release and its SHA-256 is verified before extraction, so a tampered or swapped release binary fails the job instead of running on a runner with the repository checked out.

Push runs get their own concurrency group. A superseded push run would otherwise leave its commits unscanned.

If a secret leaks anyway, the rule is the one in CONTRIBUTING.md: rotate it immediately and tell the maintainers. Removing the commit from history is not enough. Assume anything pushed is compromised.

SBOM, vulnerabilities, and licences

The supply-chain workflow generates the SBOM once and hands it to two gates.

syft scan dir:. \
  -o spdx-json=security/sbom/openrunic.spdx.json \
  -o cyclonedx-json=security/sbom/openrunic.cdx.json

The SBOM step installs dependencies first, because licence metadata lives in the installed packages rather than in the lockfile.

Vulnerabilities. grype scans the SPDX SBOM with --fail-on critical and uploads SARIF to code scanning. Accepted findings go in .grype.yaml at the repository root, which grype auto-discovers, so a local run applies the same policy:

grype sbom:security/sbom/openrunic.spdx.json --fail-on critical

Every entry under ignore: must carry a comment with three things: why the finding is acceptable, an owner, and a date at which it must be re-reviewed. The list is empty today.

Licences. grant checks the SBOM against .grant.yaml, which is deny-by-default:

grant check security/sbom/openrunic.spdx.json --config .grant.yaml

The allow list is the project's own licence family (AGPL-3.0-only, AGPL-3.0-or-later, AGPL-3.0) plus the permissive AGPL-compatible set: MIT, ISC, BSD-2-Clause, BSD-3-Clause, Apache-2.0, 0BSD, BlueOak-1.0.0, CC0-1.0, CC-BY-4.0, Unlicense, Python-2.0, and MPL-2.0 as weak copyleft.

Several entries were added after verifying them against the real dependency tree, each with its rationale recorded inline: Artistic-2.0, CC-BY-3.0 (data only), LGPL-3.0-or-later (one-way compatible, reached through prebuilt native image codecs), MIT-0, and WTFPL. Dual-licence SPDX expressions are listed verbatim where the SBOM surfaces them.

GPL and LGPL dependencies are not pre-approved. The first one to appear should be added deliberately, with its one-way-compatibility argument written down in the pull request.

require-license: false is set today so that packages with no machine-readable licence field fail on an actual conflict rather than on missing metadata. Flipping it to true is a documented follow-up once that tail is triaged.

Install-time supply-chain guards

Two guards live in pnpm-workspace.yaml.

minimumReleaseAge: 4320 refuses to install any package version published less than three days ago, which gives the ecosystem time to catch a malicious release before it reaches this repository.

onlyBuiltDependencies is an allow list of packages permitted to run postinstall scripts. pnpm 10 blocks them by default. The list is @prisma/client, @prisma/engines, prisma, esbuild, sharp, and unrs-resolver.

Action pinning

Every third-party action is pinned to a full commit SHA with the human-readable version in a trailing comment, for example:

uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

The three CLI tools downloaded at runtime (Gitleaks, syft, grype, grant) are pinned by version and verified by SHA-256 before extraction. Tool versions live in the workflow rather than in a repository script, so there is exactly one place to update them.

Token hygiene

  • Workflows declare permissions: contents: read at the top level and widen only on the jobs that need more.
  • Checkout steps pass persist-credentials: false, so the default token is not written into .git/config where a later step or action could read it.
  • Reusable workflows name individual secrets rather than taking secrets: inherit, which would put the whole organisation and repository secret store in scope.
  • The Sonar stage selects its token with static expressions rather than indexing the secrets context dynamically. A dynamic index makes the entire context reachable from that one expression, which CodeQL reports as excessive secret exposure.

OpenSSF Scorecard

Scorecard runs on pushes to main, on branch-protection changes, and weekly. It publishes to the public OpenSSF API, which is what backs the README badge.

With the default token the Branch-Protection check reads as inconclusive, because that token cannot read branch-protection or ruleset settings. Scoring it requires a fine-grained token with read access to Administration, stored as the repository secret SCORECARD_TOKEN; the workflow picks it up automatically when present.

The Scorecard workflow currently lives on the feat/emr-app branch and has not merged to dev.

Related pages

Clone this wiki locally