Add Actions workflow to update dependencies inside devcontainer#467
Add Actions workflow to update dependencies inside devcontainer#467some-natalie wants to merge 2 commits into
Conversation
|
Can we hold off until SCA lands please. 'latest' isn't necessarily the best/safest/compatible version and I'm 30+ commits deep into that journey at the moment 😄 |
Of course! I just wanted to bump CodeQL forward myself and figured I'd contribute back. LMK if you need help or a rubber duck on SCA stuff. |
|
Hey, appreciate the contribution and offer of support. I already have a CI component in the works but have added your devcontainer idea specifically. When the commit lands you will have a co-author credit on that front. But you've also led me to ponder the question of can a GH action actually do a 'super-dependabot' upgrade whilst being safe vs potential supply chain attacks? And the answer is sort of, but some of the defences need an LLM to reason (e.g. 'does this thing look like it was backdoored?') - so hard to do in that environment. To that end I'm working on an advisory model rather than a fully-automated one (with fully auto being a toggle should it be within user risk appetite). I'd definitely like some assistance once I get my dev branch off my local machine and onto GH. It is a huge area and I imagine there are a lot of things that will need tweaking/improving/iterating. In the meantime don't be surprised if a few random 'Co-Authored: by ' credits appear in your feed as I implement some of the precursors to your suggestion and they will likely land on main before the rest of the SCA branch. |
There's a lot to unpack there on safety, partly because there's not a fantastic limit on how deep Actions will resolve dependencies (or if there's vendored dependencies) and each different types of Actions all have their quirks. My approach is usually to try and let Actions agent do the resolution, then look at the delta with binary capability diffs ... which has some limitations too. I took a quick stab at this a while ago with malcontent from @tstromberg while we were both at Chainguard. Here's a workflow and PR it reviewed. My implementation of it is brittle, but I'm hoping to carve out some time to improve it soon. 😅 I'd imagine that a "super" type upgrade would be equally complex as trying to just tackle the Actions ecosystem. More food for thought! edit - thank you, and I'm happy to help, just lmk how |
Adapted from Natalie Somersall's PR #467 (dependabot-style upstream version checker). Her work covered the GitHub releases / tags endpoint shape and the cache-key + tag-stripping logic; this commit lifts that core, refactors it onto an injected HttpClient + JsonCache substrate, and adds the shared stable-semver filter that future resolvers (OCI, Helm) will reuse. First lift of a centralised "latest upstream version" lookup. Today every consumer that needs to know "what's the current stable release of $thing" reinvents the same dance: hit api.github.com/.../releases/ latest, filter out pre-release tags, strip the v prefix, cache for a day. Done in three places already (cve-diff, agentic upstream checks, the new SCA bumper) with three slightly different definitions of "stable" — drift waiting to happen. This commit adds: * core/upstream_latest/github_releases.py — GitHub releases endpoint (latest_release), tags-list fallback for projects that don't cut formal releases (latest_tag), and the resolve_tag_to_sha primitive used by GHA-uses commit-SHA pinning. * core/upstream_latest/_version_filter.py — single parse_stable / highest_stable implementation: regex ^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?$ rejecting pre-release / nightly / variant tags. One source of truth shared by every resolver here and any future ones. * core/upstream_latest/__init__.py — re-exports the public API. A follow-up lift extends this with OCI + Helm resolvers. HttpClient + JsonCache are injected, so consumers control egress policy (proxied / sandboxed) and cache locality. The lookup gracefully falls through GitHub releases → tags-list when /releases/latest returns a non-semver name like "codeql-bundle-v2.25.4". Origin: https://github.com/grokjc/raptor/pull/467 Co-Authored-By: Natalie Somersall <natalie.somersall@gmail.com>
|
Making progress. Just landed #545 to help with this. Full SCA module waiting in the wings ... not long now 😄 |
Adds a third pass to ``parse_dockerfile`` that walks ``ARG`` lines
whose name ends in ``_VERSION`` and emits ``Dependency`` rows for
those that map to an SCA ecosystem. The rows flow through the
normal OSV / KEV / EPSS pipeline.
Mapping comes from two sources:
1. **Built-in allowlist** (``_BUILTIN_ARG_MAP``) — handles the
common cases: ``SEMGREP_VERSION`` → ``PyPI:semgrep``,
``CLAUDE_CODE_VERSION`` → ``npm:@anthropic-ai/claude-code``,
etc. Boilerplate ARGs that have no SCA ecosystem (CodeQL CLI,
Python / Node / Go runtimes, base-image tags) are listed as
``None`` so they're silently skipped without needing per-line
``# raptor-sca: skip`` comments.
2. **Inline-comment override**:
ARG VENDORED_LIB_VERSION=2.0 # raptor-sca: PyPI:vendored-lib
ARG ALREADY_REVIEWED=1.0 # raptor-sca: skip
The override wins over the built-in map (operator may have
already accepted risk on a specific version).
The ARG pass runs FIRST in ``parse_dockerfile`` so that when a
Dockerfile both ``ARG FOO_VERSION=1.0`` AND
``RUN pip install foo==${FOO_VERSION}``, the concrete-version row
(``foo@1.0``) wins the canonical-dep dedup over the placeholder
row (``foo@${FOO_VERSION}``) emitted by the shell scanner. Pre-
sequence the placeholder won, causing OSV to return all-CVE
noise for the package.
ARG-derived deps carry ``source_kind="dockerfile_arg"`` and
``scope="build"`` so operators can tell them apart from runtime
app deps (same scope as Maven parent-POM coordinates).
The version regex accepts PEP440 pre-release / dev shapes
(``20.8b1``, ``20.8rc1``, ``20.8.dev0``) plus semver shapes
plus 4-part NuGet versions. Rejects ``latest``, ``main``,
branch refs, and shell expansions like ``${BASE}`` (can't query
OSV for those).
17 tests cover built-in extraction, npm scoped-name handling,
v-prefix stripping, skip-known-non-SCA, unknown-ARG silent skip,
inline override, override-skip wins, non-version rejection,
quoted values, multiple-ARGs Dockerfiles, the canonical-dedup
ordering (placeholder-vs-concrete), and PEP440 shapes.
This is the SCA-side complement to
#467 which adds a GHA
workflow that auto-bumps these same ARG pins to the latest
GitHub release. PR #467 keeps the versions current; this commit
keeps the operator informed of CVE exposure on whatever's
currently pinned. Suggested operator pattern: combine the two
— the GHA workflow refreshes versions, the next scan reports
any CVEs that the bump newly introduced (or any that the
previous pin was hiding).
Co-Authored-By: Natalie Somersall <natalie.somersall@gmail.com>
Phase 2.c: adds the rewriter registry that the bumper subcommand
will use to apply edits across opinion surfaces, and ships the
first rewriter — Dockerfile ARG version pins.
Registry shape (symmetric to ``packages/sca/parsers/``):
* ``RewriteEdit(locator, old_value, new_value)`` — a single
proposed edit. ``locator`` is rewriter-specific (ARG name
for Dockerfile, dep name for npm, group:artifact for
Maven).
* ``RewriteResult(edit, applied, reason)`` — per-edit
outcome. ``reason`` ∈ ``{"applied", "no_change",
"not_found", "value_mismatch: ...", "error: ..."}``.
* ``register(filenames=..., predicate=...)`` decorator —
same shape parsers use.
* ``rewrite(path, edits)`` dispatcher — picks the right
rewriter for the path.
Dockerfile ARG rewriter behaviour:
* ARG present at expected ``old_value`` → rewrite + applied.
* ARG already at ``new_value`` → ``no_change`` (idempotent
skip — re-running a successful plan doesn't churn the file).
* ARG present at a DIFFERENT non-target value → ``value_
mismatch``, file left alone. Operator probably bumped
manually or the plan is stale; surface the discrepancy
instead of silently overwriting.
* ARG absent from file → ``not_found``.
* I/O error → ``error: ...``.
Robustness features:
* Atomic write (tempfile + rename) so an interrupted rewrite
can't leave a half-written Dockerfile.
* Whitespace-tolerant regex (handles ``ARG FOO = 1.2.3`` with
spaces around ``=``).
* Quote-preserving (``ARG FOO="1.2.3"`` rewrites to
``ARG FOO="1.2.4"``, not ``ARG FOO=1.2.4``).
* Exact-name match — ``SEMGREP_VERSION`` doesn't accidentally
match ``CUSTOM_SEMGREP_VERSION`` or
``SEMGREP_VERSION_FALLBACK``.
Partial-failure behaviour: when a multi-edit batch has some
successes and some failures, the successful edits are persisted
(atomic write of the partially-updated content), the failed
edits return their failure ``RewriteResult``. The bumper
surfaces the partial state in the PR comment so reviewers see
what landed and what didn't.
15 tests cover: happy path, idempotency (already-at-target),
not-found skip, value-mismatch refusal, multi-edit batch,
partial-failure keeps successes, whitespace tolerance, quote
preservation, exact-name match, registry dispatch (Dockerfile +
Dockerfile.dev / .Dockerfile / .dockerfile suffix variants),
unknown-file fallthrough, atomic-write preservation of
unrelated content, re-run idempotency.
Adapted from #467 by
Natalie Somersall — her ``update_dockerfile()`` ships the
``rf"^(ARG {arg}=)(\\S+)"`` regex + idempotent
skip-if-unchanged + change-tuple-return pattern. This module
generalises that into the ``RewriteEdit``/``RewriteResult``
registry interface used across all SCA rewriters. Phase 2.c of
the dependabot++ pattern in memory
project_sca_dependabot_plus_plus.md.
Co-Authored-By: Natalie Somersall <natalie.somersall@gmail.com>
Phase 2.d: the orchestrator + CLI that ties Phases 1.a-d and
2.a-c together. End-to-end flow:
raptor-sca bump <target>
walk every Dockerfile under target
for each ARG with a known upstream source:
query upstream-latest (GitHub releases / tags)
if not already at latest → candidate
evaluate bump-tier supply-chain verdict
(recent_publish + maintainer_change + install_hook)
print verdict table (default) or JSON (--json)
optionally --apply Clean bumps in place
Pieces shipped this commit:
* ``packages/sca/bump/orchestrator.py`` — walker + verdict
computer + rewriter driver. Caches upstream-latest lookups per
(kind, coordinate) so two Dockerfiles pinning the same tool
don't double-hit the GitHub API.
* ``packages/sca/bump/upstream_map.py`` — ARG name → upstream
source mapping (GitHub releases / tags repos for each ARG in
``_BUILTIN_ARG_MAP``, plus CODEQL_VERSION which has no SCA
ecosystem but does have a GitHub-releases coordinate).
* ``packages/sca/bump/cli.py`` — argparse + dispatch to
orchestrator. ``--apply`` / ``--json`` / ``--no-cache`` /
``--cache-root`` / ``--github-token`` / ``-v``.
* CLI wire: ``bump`` added to SUBCOMMANDS in ``packages/sca/cli.py``.
Suggest-only policy (memory project_sca_dependabot_plus_plus.md):
``--apply`` writes Clean bumps in place but REFUSES to write
Review or Block bumps even when the operator passes ``--apply``.
Per-finding rationale (recent_publish detail, maintainer_change
diff) surfaces inline in the report so reviewers see WHY a
verdict isn't Clean without having to open another tool.
The orchestrator pattern is general dependabot/renovate
territory — no co-author trailer.
12 new tests cover: empty-target, unknown-ARG-skipped,
already-at-latest-no-candidate, below-latest-becomes-candidate,
recent_publish-as-Review-verdict, upstream-lookup-failure-recorded-
in-skipped, apply-writes-Clean, apply-doesn't-write-Review,
default-is-dry-run, render-report-shape, no-candidates-friendly-
message, cross-Dockerfile-upstream-dedup.
This makes Phase 2 complete — the bumper now produces operator-
useful output end-to-end. Phase 3 (additional surfaces: FROM
image refs, GHA uses, k8s image:, Helm chart deps) is each
~1-2 days of (walker + rewriter + upstream source) per surface.
End state: ``raptor-sca bump <target>`` works on raptor's own
.devcontainer/Dockerfile once Natalie's PR #467 doesn't beat us
to it — but more usefully, on any project that uses the same
ARG convention.
Phase 3.a: extends the bumper to recognize Dockerfile ``FROM
<image>:<tag>`` lines as bump candidates alongside ARG version
pins. Pulls together pieces from the prior phases:
* ``core/dockerfile/parser`` for parsing FROM instructions
* ``core/oci/image_ref.parse_image_ref`` for resolving the
registry/repository/tag triple
* ``core/upstream_latest/oci_tags.latest_tag`` (Phase 2.b) for
the upstream-latest lookup
* New ``packages/sca/rewriters/dockerfile_from.py`` for the
in-place rewrite
The orchestrator now walks both ARG pins and FROM lines; the
``BumpCandidate`` dataclass got a ``kind`` discriminator
(``"arg"`` / ``"from_image"``) and a generic ``locator`` field
that's the ARG name for ARGs and ``"{registry}/{repository}"``
for images.
Filtering on the FROM-walker side:
* Variant tags (``python:3.12-bookworm``, ``ubuntu:jammy``) —
silently skipped. We don't have a variant-tag equivalence
map; an auto-bumper that flipped ``bookworm`` → ``trixie``
would be making a non-trivial OS change without operator
input.
* Digest-pinned (``image@sha256:...``) — immutable, not a bump
target.
* Stage reuse (``FROM build AS runtime``) — references a prior
build stage, not an image.
* Tag already at latest — not a candidate.
Verdict tier: FROM-image candidates currently fall through to
Clean (no bump-tier supply-chain signals available for OCI yet
— no per-tag publish-date API on most registries). The
suggest-only policy still applies: the candidate appears in
the PR with the proposed bump, human review catches anything
weird. A future commit could add OCI-tag publish-date
detection for Docker Hub specifically (it has a separate paginated
endpoint with dates).
Rewriter dispatch refactor: ``dockerfile_arg`` and
``dockerfile_from`` would both register against the Dockerfile
predicate, but the dispatcher resolves a single rewriter per
path. Resolved by making ``dockerfile_from`` the registered
entry point that splits edits by locator shape (``/`` → FROM,
else → ARG) and delegates ARG edits to the ``dockerfile_arg``
function internally. One predicate registration, both edit
types routed correctly. Tests for both rewriters AND a mixed-
batch test that exercises the dispatch.
Live verification on raptor's own .devcontainer/Dockerfile:
FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm
The variant tag (``1-3.12-bookworm``) correctly silent-skips —
not a stable-semver shape. Once raptor's Dockerfile uses a
pure-semver tag, the bumper would surface it as a candidate.
19 orchestrator tests + 10 FROM rewriter tests cover the new
paths. Total Phase 3.a delta: ~270 lines, no co-author trailer
(no prior art from PR #467 for FROM rewrites; ARG path was
the only piece her work shaped).
Closes PR #467's deferred 5th-Tier-1-signal item: when a dep bumps from X to Y and the deliverable is a binary artifact (Dockerfile FROM image, GHA Docker-action, pre-built release pin), run ``BinaryUnderstand`` against current + target binaries and flag the bump when target adds dangerous capabilities the current didn't have. Substrate - ``packages/sca/bump/binary_capability_delta.py`` — three-layer module: * ``_bucket_imports(imports)`` — classify import set by the shared ``core.function_taxonomy`` capability buckets (exec, network, string_overflow, scan, memory_copy, format_string, alloc, parser, integer_parse, toctou). Ubiquitous funcs (``malloc`` / ``printf`` / ``read``) deliberately dropped — not signal. * ``diff_binary_capabilities(cur, tgt)`` — analyse both binaries via ``packages.binary_analysis.radare2_understand. analyse_binary_context``, return a ``CapabilityDelta`` carrying new buckets + new reachable sinks. Returns None when radare2 unavailable or either binary unanalysable (graceful degradation); empty delta when both analyse but target adds nothing. * ``binary_capability_delta_finding(...)`` — wrap the delta in a ``SupplyChainFinding`` of kind ``"binary_capability_delta"`` (new entry in ``SupplyChainKind`` literal). Severity ladder: - new exec or network capability → ``high`` (RCE / exfil-flavoured) - other dangerous capability → ``medium`` Confidence ``medium`` — static signal only, no runtime confirmation. What this isn't - The wiring into ``run_bump`` / ``_evaluate_one_candidate`` isn't done yet. Driving the detector for Dockerfile FROM bumps needs OCI image-config introspection (read CMD / ENTRYPOINT) + targeted binary extraction from the layer. That's a separate ~100 LOC and lives in a follow-up commit. - Path resolution is the caller's responsibility — the detector takes already-extracted ``Path`` objects, doesn't pull / unpack images itself. Clean separation of concerns lets the same detector serve Docker-FROM, GHA-action, and binary-artifact-pin call sites with their own extractors. Tests - 19 unit tests via stubbed BinaryContextMap (no radare2 required for the unit suite): * bucket classification (exec, network, string_overflow, ubiquitous dropped) * diff outcomes: no change, new exec, new network, new string_overflow, new sinks, removed capabilities ignored, multiple buckets * graceful degradation: radare2 unavailable, current analyse fail, target analyse fail * finding wrapper: high severity for exec, medium for string_overflow, no finding when unchanged, detail rendering - 1 integration test gated on probe_capability() — diffs /bin/ls against itself, asserts empty delta (catches non-determinism). Skips on hosts without r2pipe. Co-Authored-By: Natalie Somersall <natalie.somersall@gmail.com>
Closes the GHA half of PR #467's signal: ``uses: owner/repo@vX`` candidates that point at Docker-container actions now flow through the same capability-diff machinery as Dockerfile FROM bumps. This matches what Natalie's malcontent-actions workflow covered. What GHA actions look like - JavaScript / composite actions (``runs.using: node20`` / ``composite``) — ship as a git repo of JS/YAML, no OCI image. Out of scope; the resolver returns None and the bumper emits no capability-delta finding (other signals still apply). - Docker-container actions with a pre-built image (``runs.using: docker`` + ``runs.image: docker://image:tag`` or ``image:tag``) — exactly the shape we can diff. Resolver returns an ``image_ref`` ready for ``fetch_image_binary``. - Docker-container actions with a Dockerfile reference (``runs.image: Dockerfile`` / ``./Dockerfile`` / ``build/ Dockerfile``) — image only exists post-build; we don't run builds. Resolver returns None. Substrate - ``packages/sca/bump/gha_action_image.py``: * ``resolve_gha_action_image(repo, ref, *, http)`` — fetch ``action.yml`` (fall back to ``action.yaml``) from raw.githubusercontent.com, parse the YAML, classify the ``runs`` shape, return a ``GhaActionImage`` carrying the cleaned OCI ref. Unauthenticated fetch — fine for public repos which are the capability-delta-relevant case. * ``_parse_docker_action_image(text)`` — pure-YAML parse function isolated for direct unit coverage. Handles all the GHA syntax variants: ``docker://`` URI prefix, plain ``image:`` form, case-insensitive ``using``, Dockerfile rejections (bare, relative path, subdir, ``.dockerfile`` extension). - ``_binary_capability_delta_for_candidate`` extended to dispatch on ``cand.kind == "gha_uses"`` via a new ``_resolve_gha_image_refs`` helper. Both halves must resolve cleanly — if the action SWITCHED between Docker and JS between versions, the resolver returns None on the mismatching side and the bumper bails (different shapes, can't capability-diff). - ``_evaluate_one`` gains an ``http`` kwarg threaded through from ``run_bump`` so the resolver can make its raw-content fetches. - ``run_bump``'s OCI-client lazy-construct gate now also fires for ``gha_uses`` candidates. Out of scope this revision - Sub-action paths (``uses: owner/repo/sub/dir@vX``): the existing GHA-uses enumerator drops the subpath when building candidates, so we read ``action.yml`` at the repo root only. Sub-action resolution requires a candidate-shape change; deferred until the enumerator carries the subpath. - Private-repo authentication: raw.githubusercontent.com doesn't auth, which is fine for the public-action case (private actions aren't the supply-chain risk we're targeting here). Tests - 20 resolver tests (``test_gha_action_image.py``): docker URI stripping, plain image, JS / composite rejection, three Dockerfile-reference shapes, ``.yml`` → ``.yaml`` fallback, both-extensions-fail, malformed YAML, non-mapping top-level, empty image, unicode decode failure. - 2 orchestrator wiring tests: gha_uses Docker action triggers resolver + extractor + detector with the right call sequence; gha_uses JS action skips the OCI fetch entirely (resolver returns None for current → bail before target). - 183 bump tests pass. Co-Authored-By: Natalie Somersall <natalie.somersall@gmail.com>
|
Hi @some-natalie - now that #595 (the SCA behemoth) has landed, I want to close this out properly, because your PR turned into a lot more than one workflow's worth of value and I want to be specific about what survived and where it lives. Your update_devcontainer.yml + update_dockerfile() was the proof-of-shape for a general "safe auto-bump" loop. We took the cron → branch → resolve-latest → rewrite → PR-open pattern and generalised it across eight version-pin surfaces instead of just the devcontainer:
Every surface runs the same loop your Dockerfile code prototyped: walk → find pin → upstream-latest lookup → verdict → idempotent in-place rewrite. The difference from vanilla dependabot/renovate is the verdict gate. Before a bump is applied it's checked against the full SCA signal set with no LLM in the CI path: OSV / KEV / EPSS, yanked-release, license policy, install-hook delta, typosquat / compromised-package feed, maintainer-change, version-diff size, binary-capability-delta (more on that below), and function-level reachability. Only Clean verdicts get written; Review/Block leave the finding for a human. The workflow itself .github/workflows/sca-self-bump.yml — directly descended from your update_devcontainer.yml:
Where your work is in the tree You're mentinoed via Co-Authored-By: on 17 commits on main. The load-bearing ones:
And separately, your malcontent / capability-diff suggestion from the thread became a whole feature line, not just a footnote. Rather than bundle malcontent we reused our existing radare2-based binary-understanding substrate to diff the syscall/network/file-IO capability surface of current vs target; a bump that suddenly grows capabilities (network where there was none, new execve-family calls, etc.) escalates the verdict. That's the binary-capability-delta detector + the fingerprint CLI + the CycloneDX capability embedding + the --fail-on-capability-drift gate — all attributed to you, since the idea was yours. Closing this PR I'm closing #467 as superseded by #595 because the equivalents are already wired into the broader bumper and merging the original on top would conflict and undo the wider feature. But the design and the idea both shipped, at considerably larger scope than the original diff. Thank you for the idea, and the inspiration. As for your offer of help, I'm sure there are many bugs lurking here, it was a significant piece of work. |
This PR adds a Python script and GitHub Actions workflow to update the hard-coded dependencies inside of
.devcontainer/Dockerfileby opening a PR like a janky Dependabot. I use scripts like this to reduce toil on maintaining pinned deps.