Skip to main content

Supply-chain security in CI

CI runs these controls on pull requests, merge groups, main pushes and releases. CI/CD architecture describes the pipeline that runs them.

Controls​

ControlTimingEnforcementDestination
Dependency vulnerability gateSecurity workflowowns the dependency verdict. Blocks fixable high or critical findings in every dependency the checkout resolvesrequired Security workflow
GitHub dependency reviewpull requests that change a dependency inputreports the advisories a pull request introduces, with their patched versions. Holds no verdictIndependent dependency-review summary
TruffleHog and GitHub push protectionpull requests and pushesblock verified secretsSecurity workflow and repository protection
Trivy filesystem scanSecurity workflowa scanner error blocks. Findings of every severity are report-onlycode scanning
Semgrep TLS policypull requests and main pushesfails on a rule violation or a scanner errorcode scanning and a run artifact
Image vulnerability policyimage build and releaseblocks fixable high or critical findingsbuild summary and release evidence
Weekly image rescansrescan-images.ymlthe main rescan upserts one tracking issue. The release rescan fails and comments on the vulnerability response issueissues and run artifacts
OpenSSF Scorecardmain pushes, branch-protection changes, and weekly. Never on pull requestsreportscode scanning and a run artifact
Scorecard ratchetthe same events, plus on demandfails when a check scores below security/scorecard-baseline.json. On a push, only for the checks that commit decidesjob summary and a run artifact
Renovateconfigured schedule and vulnerability alertspull requests require normal review and CIdependency dashboard

Image scanning also reads embedded SBOMs shipped by image builders. Trivy's third-party-SBOM advisory describes that additional inventory's provenance. It does not mean the image was scanned only through the separately published release SBOM. Keep those embedded inventories and the scanner's vendor-severity provenance visible rather than changing severity sources or disabling analyzers to remove a warning. The vulnerability policy result and retained scanner JSON carry the actual verdict.

One check owns the dependency verdict: the dependency vulnerability gate in ci-security-scan.yml, a second Trivy pass in table format over the same checkout. It runs on pull requests and on main pushes and includes development and test dependencies from the lockfiles.

The native server/buildscript-gradle.lockfile also covers the resolved Gradle plugin classpath. Trivy reads this file through its Gradle lockfile analyzer. Application lockfiles and container scans do not cover build plugins.

An advisory published against a pin no pull request touches fails the next run rather than waiting for someone to open the code-scanning dashboard. It applies the vulnerability remediation severity policy: fixable high or critical. security/trivy-dependency-ignore.yaml records its exceptions, the sole exception path for dependency findings.

Dependency review compares the base and head dependency graphs, so it cannot hold that verdict. It reports what the pull request introduces in the independent dependency-review.yml workflow. Asynchronous snapshots can keep this report pending, but it does not delay CI Status Gate, CI/CD completion, or release qualification.

The comparison runs only when a pull request changes a file that the dependency graph reads. These files are package manifests and lockfiles, workflows and actions, and the Gradle build scripts, lockfiles and version catalog. Any other change has an empty comparison. dependency-graph.yml uses the same pull-request filter. Thus, the head snapshot that the review waits for exists whenever the review runs. scripts/ci-contract.test.ts holds both filters to one list.

Those filters use GitHub's bounded changed-file diff. A very large mixed change can omit this informational report. The required full-checkout security and release policies do not depend on that filter.

Gradle graphs are generated on pull requests and main pushes through the official Gradle action, with dependency verification still enabled. The workflow runs dependencies buildEnvironment to include the plugin classpath. Generation has read-only permissions.

A separate workflow_run publisher submits the snapshot without checking out or executing the originating code. The publisher must exist on the default branch before GitHub runs it.

The first Trivy pass supplies the code-scanning feed. SARIF reports every severity regardless of severity. Thus, this pass sets neither severity nor exit-code. It keeps Trivy's secret scanner, which covers the whole tree. TruffleHog covers only a pull request's own commits.

TruffleHog has two independent pins in ci-security-scan.yml: the action commit selects its wrapper, while the supported image and version inputs select the scanner container by tag and digest. The wrapper otherwise defaults to a mutable scanner tag even when its action is pinned. Renovate tracks the wrapper through its GitHub Actions manager and the scanner through a Docker custom manager. Their versions need not move together.

When you review a wrapper update, verify its image and version input contract. The resulting Docker reference must retain the digest. A scanner failure must still fail the step. The event's immutable base/head commit range, verified-secret policy, and scan-error failure policy apply to either update.

Renovate groups routine Gradle updates as server dependencies and webapp development updates separately. Webapp production dependencies remain separate unless Renovate identifies a package family that must move together.

Major updates and minor updates to pre-1.0 dependencies remain ungrouped, wait seven days, and require Dependency Dashboard approval. Native branch and PR limits bound routine update concurrency. The hourly limit only restricts PR creation. Vulnerability alerts remain ungrouped and bypass those gates and limits.

pnpm's minimumReleaseAge in pnpm-workspace.yaml still applies to a version that a lockfile update newly resolves. A fix younger than that age opens as a pull request with a failed lockfile update. To take it earlier, a maintainer adds the version to minimumReleaseAgeExclude.

Lockfile maintenance runs on Mondays. .github/renovate.json owns the managed ecosystems and extraction rules. scripts/renovate-config.test.ts exercises each custom manager against its source.

Renovate runs as the Mend-hosted app on the Community plan. A job has 3 GB of memory and 30 minutes. A job that exceeds a limit stops before it writes pull requests or the Dependency Dashboard. Mend then runs the repository only weekly.

If the Dependency Dashboard stops changing, read the latest job on the Mend Developer Portal. The job page shows its status, such as kernel-out-of-memory, and its log. After you fix the cause, select Retry on that job. A successful job returns the repository to the normal schedule. trustLockfile in pnpm-workspace.yaml keeps the pnpm lockfile updates in a job inside the memory limit.

The Node, pnpm and Pi SDK pins are grouped, so a version stated in several managed files moves in one pull request. The toolchain checks reject a version that reads differently in any of them. Renovate provisions the Node it runs lockfile updates with from engines, so engines.node restates devEngines.runtime exactly rather than the supported line.

For Gradle, constraints.java restates .java-version, enforced by gate:toolchain: Renovate's parser cannot infer the toolchain through the file read in the Kotlin build. The Gradle and wrapper managers own version, lockfile and verification-metadata updates.

A self-hosted Renovate administrator must permit gradleWrapper in allowedUnsafeExecutions for artifact updates. A repository config cannot grant that permission.

Missing or failed artifact updates are not a reason to weaken Gradle verification. Follow Updating Java dependencies to repair and verify them before merging.

Renovate-generated checksums still require independent review.

The Vulnerability remediation guide owns the severity policy and exception shape, ceiling, and approval for images and dependencies.

The Release management guide owns artifact signing, provenance, SBOM, and release-verification guarantees.

Semgrep policy​

security/semgrep/ holds this repository's own rule pack: syntax checks for disabled Node certificate verification and unconditional Java hostname verifiers, not comprehensive TLS analysis. The image-reference and Liquibase guards in ci-security-scan.yml stay independent. Inline nosemgrep suppression is disabled, so an exception is a reviewed rule change, and every rule carries a positive and a negative fixture beside it.

Semgrep reports. It is not a required check. The digest-pinned scanner runs without network access or credentials over a read-only checkout, and .github/workflows/semgrep.yml owns the commands that reproduce a run locally. A pull request from a fork gets the scan verdict but no code-scanning row, because GitHub caps a fork token's security-events access at read.

Static analysis coverage​

CodeQL analysis is disabled: CI has no extraction, query-analysis or scheduled CodeQL job, and GitHub's CodeQL default setup must remain disabled. This removes CodeQL's interprocedural security analysis. The remaining checks are not an equivalent replacement. GitHub Code Quality is a separate maintainability service and remains enabled.

The pinned github/codeql-action/upload-sarif steps publish results from other scanners. They do not run CodeQL analysis. Keep those uploads so Semgrep, Trivy and Scorecard retain their reporting. Normal Java builds retain NullAway and fatal compiler warnings without an extraction-only bypass.

Scorecard baseline​

security/scorecard-baseline.json owns the minimum every enforced Scorecard check must hold, the exclusions with their reasons, and what each enforced check is scored over. Checks are compared one at a time, so an improvement cannot offset a regression, and a check the baseline does not name is not enforced. Lowering a minimum also edits the map in scripts/check-scorecard.test.ts, so the change is visible in the pull request that makes it.

scoredOver is configuration when Scorecard computes the score from current repository files, workflows, and branch rules. It is history when Scorecard uses a window:

  • SAST and Code-Review: about 30 most recent merged pull requests and commits.
  • CI-Tests: about 30 most recent commits.
  • Signed-Releases: 30 most recent releases.
  • Maintained: the previous 90 days.

Each enforced check must carry one classification and a one-line reason. Without these, the script fails. Thus, a new baseline check gets a deliberate classification, not a default.

The classification determines responsibility for a regression. On push, only a configuration check fails the ratchet, because that commit determines repository configuration. A history check appears in the job summary, but the run remains green. No single commit can control the aggregate of the last thirty. Failure on main would fail every merge until the window advances.

On schedule, workflow_dispatch, and branch_protection_rule, every check can fail. These runs check again after time for window recovery. A score still below baseline represents a trend, not a merge. Outside Actions, without GITHUB_EVENT_NAME, the script enforces everything.

scripts/check-scorecard.ts reads OpenSSF's latest published assessment for this repository, not necessarily the current commit. It fails on any of these conditions:

  • A regression attributable to the event.
  • A missing enforced check.
  • Evidence older than the baseline or more than eight days old.
  • An API or parse failure.

scorecard-ratchet.yml runs on main pushes, branch-protection changes, weekly, and on demand. It retains the fetched assessment as an artifact. It needs its own workflow because OpenSSF restricts the publishing job to allowlisted actions. Reproduce it with node scripts/check-scorecard.ts. Alternatively, run node --test scripts/check-scorecard.test.ts without network access.

Gradle dependency inventory and the Vulnerabilities exclusion​

Scorecard's Vulnerabilities check does not read the dependency graph submitted to GitHub or Dependabot's alerts. The pinned action's dependencies select two tools. These are Scorecard 5.5.0 and OSV Scanner 2.3.2. Scorecard's OSV client scans the checkout recursively and queries its Git commit. The Gradle verification-metadata extractor turns every component's group, name and version in server/gradle/verification-metadata.xml into a package, ignoring its artifact entries and whether Gradle selected that version.

That file is a checksum allowlist, not a resolved dependency graph. Gradle can read an older version's POM or module descriptor while it selects a patched JAR. Metadata generation also only adds entries. It keeps the entry of a version that no build downloads any more. OSV reports both kinds as dependencies.

Quality → App Server checks this file on a pull request that changes Gradle inputs. It fails on an entry that a clean dependency resolution does not download. The merge group skips this check, because the pull request already decides it. Updating Java dependencies gives the command that removes it. Do not remove an entry that a fresh build reads: dependency verification then fails.

Reproduce the distinction for the Gradle sources from the repository root with the OSV Scanner release used by the action:

osv-scanner scan source --format json --all-packages \
--lockfile server/application/gradle.lockfile \
--lockfile server/generated-clients/gradle.lockfile \
--lockfile server/buildscript-gradle.lockfile \
--lockfile server/gradle/verification-metadata.xml

Each result names its source.path, package coordinates and version, and any advisory's affected ranges. --all-packages also includes the patched lockfile entries, so compare the same package across sources rather than treating every reported version as selected. Exit 1 means vulnerabilities were reported. An extraction or network error is not a clean assessment.

The advisories investigated in #2078 all came from verification metadata, not either application lockfile. The plugin classpath now has its own native lockfile, server/buildscript-gradle.lockfile. server/build.gradle.kts aligns both Jackson API families with patched BOMs and constrains other generator dependencies. Gradle requires separate activation for buildscript locking. Check ./server/gradlew -p server buildEnvironment when diagnosing these packages.

Re-run the checks when the action or dependencies change. The recorded observation is not an exemption for a new advisory against a resolved package.

This inventory limitation justifies excluding the Scorecard Vulnerabilities check, not relaxing other gates. The baseline records that reason. Its other exclusions have separate policy reasons. The dependency vulnerability gate and release image policy remain enforced.

Restore the check when the OSV Scanner run above reports no advisory. Do not alter dependency submission or disable checksum verification to change its score.