Skip to content

Commit efcf13c

Browse files
authored
Merge branch 'main' into fix/DEVA11Y-484-bsdtar-size-limit
2 parents 2c5fba8 + e8ff791 commit efcf13c

3 files changed

Lines changed: 156 additions & 4 deletions

File tree

.github/dependabot.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Dependabot configuration.
2+
#
3+
# Rotates the pinned GitHub Actions used across .github/workflows (DEVA11Y-476 /
4+
# chain DEVA11Y-485). Action `uses:` pins are pinned to immutable commit SHAs for
5+
# supply-chain integrity; a static SHA never receives upstream security patches, so
6+
# Dependabot's `github-actions` ecosystem opens PRs that bump each pin as new
7+
# releases ship.
8+
#
9+
# NOTE: Dependabot cannot rotate the `container:` image digest in Semgrep.yml — its
10+
# `docker` ecosystem only discovers Dockerfiles/Containerfiles, Kubernetes manifests,
11+
# Helm values and Compose files, not image refs in workflow files
12+
# (dependabot-core#5819). That digest is watched by .github/workflows/semgrep-image-pin-drift.yml
13+
# instead, which alarms when the pinned image goes stale so a human bumps it deliberately.
14+
version: 2
15+
updates:
16+
# GitHub Actions `uses:` pins in .github/workflows (directory "/" — the ecosystem
17+
# searches the workflows dir itself).
18+
- package-ecosystem: "github-actions"
19+
directory: "/"
20+
schedule:
21+
interval: "weekly"
22+
# INERT for the github-actions ecosystem: `cooldown` is NOT among the ecosystems
23+
# GitHub supports it for (docker/bundler/npm/pip/… do; github-actions does not),
24+
# so this key does nothing here. It is kept ONLY to satisfy Semgrep's
25+
# `dependabot-missing-cooldown` rule (a schema-level check that does not know the
26+
# key is ecosystem-inert). It provides no actual release-age protection for action
27+
# bumps — that would require Renovate's `minimumReleaseAge`. Do not treat actions
28+
# bumped by this config as having aged past a cooldown window.
29+
cooldown:
30+
default-days: 7
31+
commit-message:
32+
prefix: "chore(deps)"
33+
labels:
34+
- "dependencies"

.github/workflows/Semgrep.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ jobs:
2828
container:
2929
# Pinned by digest for supply-chain integrity (DEVA11Y-476).
3030
# To update: docker manifest inspect returntocorp/semgrep:latest
31-
image: returntocorp/semgrep@sha256:f682953ce85e3725f4a4dd94bd7ad13e570bb6b2c7a8cf7c6e38a9eac89239b2
31+
image: returntocorp/semgrep@sha256:f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972ade
3232

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

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

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Alarms when the pinned Semgrep CI container image digest in Semgrep.yml goes STALE
2+
# (DEVA11Y-476 / chain DEVA11Y-485).
3+
#
4+
# Dependabot cannot rotate a `container:` image digest in a workflow file
5+
# (dependabot-core#5819), so instead of auto-adopting whatever `latest` resolves to
6+
# — undesirable for the C-001 threat model, where a freshly-poisoned upstream tag is
7+
# the risk — this job keeps a human in the loop. It does NOT fail merely because
8+
# `latest` has moved (returntocorp/semgrep is rebuilt ~weekly, so a "differs from
9+
# latest" alarm would be red most weeks and get muted). Instead it fails only when
10+
# the *pinned* image is older than MAX_AGE_DAYS, which is actionable ("your pin is N
11+
# days stale") rather than noise ("upstream pushed yesterday"). Read-only; no write
12+
# permissions.
13+
#
14+
# OPS CAVEAT: GitHub disables scheduled workflows after 60 days of repository
15+
# inactivity, and a failed scheduled run only notifies whoever last edited the cron
16+
# (no team routing here). So this alarm is a backstop, NOT a substitute for the
17+
# committed digest pin in Semgrep.yml — do not rely on it as the sole control. If
18+
# stronger routing is wanted later, add an issue-creating or Slack step (would need
19+
# additional permissions, intentionally omitted here to keep this job read-only).
20+
name: Semgrep image pin freshness
21+
on:
22+
schedule:
23+
- cron: "0 7 * * 1" # Mondays 07:00 UTC
24+
workflow_dispatch:
25+
permissions:
26+
contents: read
27+
jobs:
28+
freshness:
29+
name: Pinned Semgrep image age
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 5
32+
steps:
33+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34+
- name: Fail if the pinned Semgrep image digest is stale
35+
run: |
36+
# NOTE: GitHub runs this as `bash -e {0}`; `set -uo pipefail` does not clear
37+
# that injected -e. Every command substitution whose failure must be handled
38+
# by a guard below (rather than aborting the step with no annotation) is
39+
# suffixed with `|| true` — otherwise a non-matching grep, a SIGPIPE from
40+
# `| head`, or a registry error would exit the step before the guard runs,
41+
# leaving a bare red square with no ::error:: (the trap fixed in PR #37).
42+
set -uo pipefail
43+
MAX_AGE_DAYS=45
44+
repo="returntocorp/semgrep"
45+
46+
pinned=$(grep -oE 'returntocorp/semgrep@sha256:[0-9a-f]{64}' \
47+
.github/workflows/Semgrep.yml | head -n1 | cut -d@ -f2 || true)
48+
if [ -z "${pinned:-}" ]; then
49+
echo "::error::Could not find a returntocorp/semgrep@sha256 pin in .github/workflows/Semgrep.yml"
50+
exit 1
51+
fi
52+
53+
token=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
54+
| jq -r .token || true)
55+
if [ -z "${token:-}" ] || [ "${token}" = "null" ]; then
56+
echo "::error::Could not obtain a Docker registry auth token for ${repo} (registry auth failure)"
57+
exit 1
58+
fi
59+
60+
man=$(curl -fsS -H "Authorization: Bearer ${token}" \
61+
-H 'Accept: application/vnd.oci.image.index.v1+json' \
62+
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
63+
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
64+
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
65+
"https://registry-1.docker.io/v2/${repo}/manifests/${pinned}" || true)
66+
if [ -z "${man:-}" ]; then
67+
echo "::error::Could not fetch the manifest for pinned digest ${pinned} (registry unreachable or digest gone)"
68+
exit 1
69+
fi
70+
71+
# Multi-arch index: descend into the linux/amd64 child image manifest.
72+
child=$(printf '%s' "${man}" \
73+
| jq -r '(.manifests // [])[] | select(.platform.os=="linux" and .platform.architecture=="amd64") | .digest' \
74+
| head -n1 || true)
75+
if [ -n "${child:-}" ] && [ "${child}" != "null" ]; then
76+
imgman=$(curl -fsS -H "Authorization: Bearer ${token}" \
77+
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
78+
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
79+
"https://registry-1.docker.io/v2/${repo}/manifests/${child}" || true)
80+
else
81+
imgman="${man}"
82+
fi
83+
if [ -z "${imgman:-}" ]; then
84+
echo "::error::Could not fetch the image manifest for pinned digest ${pinned}"
85+
exit 1
86+
fi
87+
88+
cfg=$(printf '%s' "${imgman}" | jq -r '.config.digest // empty' || true)
89+
if [ -z "${cfg:-}" ]; then
90+
echo "::error::Could not locate the image config descriptor for pinned digest ${pinned}"
91+
exit 1
92+
fi
93+
94+
created=$(curl -fsS -L -H "Authorization: Bearer ${token}" \
95+
"https://registry-1.docker.io/v2/${repo}/blobs/${cfg}" | jq -r '.created // empty' || true)
96+
if [ -z "${created:-}" ]; then
97+
echo "::error::Could not read the created timestamp from the image config for ${pinned}"
98+
exit 1
99+
fi
100+
101+
created_epoch=$(date -u -d "${created}" +%s 2>/dev/null || true)
102+
if [ -z "${created_epoch:-}" ]; then
103+
echo "::error::Could not parse the image created timestamp: ${created}"
104+
exit 1
105+
fi
106+
now_epoch=$(date -u +%s)
107+
age_days=$(( (now_epoch - created_epoch) / 86400 ))
108+
109+
echo "pinned digest: ${pinned}"
110+
echo "image created: ${created} (${age_days} days ago)"
111+
if [ "${age_days}" -gt "${MAX_AGE_DAYS}" ]; then
112+
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"
113+
exit 1
114+
fi
115+
echo "::notice::Pinned Semgrep image is ${age_days} days old (<= ${MAX_AGE_DAYS}); no action needed."

0 commit comments

Comments
 (0)