Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 34 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Dependabot configuration.
#
# Rotates the pinned GitHub Actions used across .github/workflows (DEVA11Y-476 /
# chain DEVA11Y-485). Action `uses:` pins are pinned to immutable commit SHAs for
# supply-chain integrity; a static SHA never receives upstream security patches, so
# Dependabot's `github-actions` ecosystem opens PRs that bump each pin as new
# releases ship.
#
# NOTE: Dependabot cannot rotate the `container:` image digest in Semgrep.yml — its
# `docker` ecosystem only discovers Dockerfiles/Containerfiles, Kubernetes manifests,
# Helm values and Compose files, not image refs in workflow files
# (dependabot-core#5819). That digest is watched by .github/workflows/semgrep-image-pin-drift.yml
# instead, which alarms when the pinned image goes stale so a human bumps it deliberately.
version: 2
updates:
# GitHub Actions `uses:` pins in .github/workflows (directory "/" — the ecosystem
# searches the workflows dir itself).
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# INERT for the github-actions ecosystem: `cooldown` is NOT among the ecosystems
# GitHub supports it for (docker/bundler/npm/pip/… do; github-actions does not),
# so this key does nothing here. It is kept ONLY to satisfy Semgrep's
# `dependabot-missing-cooldown` rule (a schema-level check that does not know the
# key is ecosystem-inert). It provides no actual release-age protection for action
# bumps — that would require Renovate's `minimumReleaseAge`. Do not treat actions
# bumped by this config as having aged past a cooldown window.
cooldown:
Comment thread
Crash0v3rrid3 marked this conversation as resolved.
default-days: 7
commit-message:
prefix: "chore(deps)"
labels:
- "dependencies"
11 changes: 7 additions & 4 deletions .github/workflows/Semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ jobs:
container:
# Pinned by digest for supply-chain integrity (DEVA11Y-476).
# To update: docker manifest inspect returntocorp/semgrep:latest
image: returntocorp/semgrep@sha256:f682953ce85e3725f4a4dd94bd7ad13e570bb6b2c7a8cf7c6e38a9eac89239b2
image: returntocorp/semgrep@sha256:f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972ade

# Skip any PR created by dependabot to avoid permission issues:
if: (github.actor != 'dependabot[bot]')

steps:
# Fetch project source with GitHub Actions Checkout.
Expand All @@ -46,5 +44,10 @@ jobs:
uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0
with:
sarif_file: semgrep.sarif
if: always()
# SARIF upload needs security-events: write, which Dependabot-triggered runs
# do not get; skip only this privileged step for dependabot so the scan itself
# still runs on Dependabot PRs (which bump the action pins that live in this
# very workflow). always() keeps upload on non-dependabot runs even if the scan
# step reports findings.
if: always() && github.actor != 'dependabot[bot]'

115 changes: 115 additions & 0 deletions .github/workflows/semgrep-image-pin-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Alarms when the pinned Semgrep CI container image digest in Semgrep.yml goes STALE
# (DEVA11Y-476 / chain DEVA11Y-485).
#
# Dependabot cannot rotate a `container:` image digest in a workflow file
# (dependabot-core#5819), so instead of auto-adopting whatever `latest` resolves to
# — undesirable for the C-001 threat model, where a freshly-poisoned upstream tag is
# the risk — this job keeps a human in the loop. It does NOT fail merely because
# `latest` has moved (returntocorp/semgrep is rebuilt ~weekly, so a "differs from
# latest" alarm would be red most weeks and get muted). Instead it fails only when
# the *pinned* image is older than MAX_AGE_DAYS, which is actionable ("your pin is N
# days stale") rather than noise ("upstream pushed yesterday"). Read-only; no write
# permissions.
#
# OPS CAVEAT: GitHub disables scheduled workflows after 60 days of repository
# inactivity, and a failed scheduled run only notifies whoever last edited the cron
# (no team routing here). So this alarm is a backstop, NOT a substitute for the
# committed digest pin in Semgrep.yml — do not rely on it as the sole control. If
# stronger routing is wanted later, add an issue-creating or Slack step (would need
# additional permissions, intentionally omitted here to keep this job read-only).
name: Semgrep image pin freshness
on:
schedule:
- cron: "0 7 * * 1" # Mondays 07:00 UTC
workflow_dispatch:
permissions:
contents: read
jobs:
freshness:
name: Pinned Semgrep image age
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Fail if the pinned Semgrep image digest is stale
run: |
# NOTE: GitHub runs this as `bash -e {0}`; `set -uo pipefail` does not clear
# that injected -e. Every command substitution whose failure must be handled
# by a guard below (rather than aborting the step with no annotation) is
# suffixed with `|| true` — otherwise a non-matching grep, a SIGPIPE from
# `| head`, or a registry error would exit the step before the guard runs,
# leaving a bare red square with no ::error:: (the trap fixed in PR #37).
set -uo pipefail
MAX_AGE_DAYS=45
repo="returntocorp/semgrep"

pinned=$(grep -oE 'returntocorp/semgrep@sha256:[0-9a-f]{64}' \
.github/workflows/Semgrep.yml | head -n1 | cut -d@ -f2 || true)
if [ -z "${pinned:-}" ]; then
echo "::error::Could not find a returntocorp/semgrep@sha256 pin in .github/workflows/Semgrep.yml"
exit 1
fi

token=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
| jq -r .token || true)
if [ -z "${token:-}" ] || [ "${token}" = "null" ]; then
echo "::error::Could not obtain a Docker registry auth token for ${repo} (registry auth failure)"
exit 1
fi

man=$(curl -fsS -H "Authorization: Bearer ${token}" \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
"https://registry-1.docker.io/v2/${repo}/manifests/${pinned}" || true)
if [ -z "${man:-}" ]; then
echo "::error::Could not fetch the manifest for pinned digest ${pinned} (registry unreachable or digest gone)"
exit 1
fi

# Multi-arch index: descend into the linux/amd64 child image manifest.
child=$(printf '%s' "${man}" \
| jq -r '(.manifests // [])[] | select(.platform.os=="linux" and .platform.architecture=="amd64") | .digest' \
| head -n1 || true)
if [ -n "${child:-}" ] && [ "${child}" != "null" ]; then
imgman=$(curl -fsS -H "Authorization: Bearer ${token}" \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
"https://registry-1.docker.io/v2/${repo}/manifests/${child}" || true)
else
imgman="${man}"
fi
if [ -z "${imgman:-}" ]; then
echo "::error::Could not fetch the image manifest for pinned digest ${pinned}"
exit 1
fi

cfg=$(printf '%s' "${imgman}" | jq -r '.config.digest // empty' || true)
if [ -z "${cfg:-}" ]; then
echo "::error::Could not locate the image config descriptor for pinned digest ${pinned}"
exit 1
fi

created=$(curl -fsS -L -H "Authorization: Bearer ${token}" \
"https://registry-1.docker.io/v2/${repo}/blobs/${cfg}" | jq -r '.created // empty' || true)
if [ -z "${created:-}" ]; then
echo "::error::Could not read the created timestamp from the image config for ${pinned}"
exit 1
fi

created_epoch=$(date -u -d "${created}" +%s 2>/dev/null || true)
if [ -z "${created_epoch:-}" ]; then
echo "::error::Could not parse the image created timestamp: ${created}"
exit 1
fi
now_epoch=$(date -u +%s)
age_days=$(( (now_epoch - created_epoch) / 86400 ))

echo "pinned digest: ${pinned}"
echo "image created: ${created} (${age_days} days ago)"
if [ "${age_days}" -gt "${MAX_AGE_DAYS}" ]; then
echo "::error::Pinned Semgrep image is ${age_days} days old (> ${MAX_AGE_DAYS}) — re-resolve returntocorp/semgrep:latest and bump the digest in .github/workflows/Semgrep.yml"
exit 1
fi
echo "::notice::Pinned Semgrep image is ${age_days} days old (<= ${MAX_AGE_DAYS}); no action needed."
Loading