From 71cc5b90f58df8ba060e565ed0ff5070b5a6dc43 Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:40:18 +0000 Subject: [PATCH] Add independent Agent Rust supply-chain checks --- .github/workflows/agent-supply-chain.yml | 46 ++++++++++++++++++++ apps/maple-agent/README.md | 8 ++++ apps/maple-agent/deny.toml | 26 +++++++++++ scripts/ci/agent_change_detection.py | 1 + scripts/ci/test_agent_change_detection.py | 1 + scripts/ci/test_agent_workflows.py | 53 +++++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 .github/workflows/agent-supply-chain.yml create mode 100644 apps/maple-agent/deny.toml diff --git a/.github/workflows/agent-supply-chain.yml b/.github/workflows/agent-supply-chain.yml new file mode 100644 index 000000000..f128f4047 --- /dev/null +++ b/.github/workflows/agent-supply-chain.yml @@ -0,0 +1,46 @@ +name: Agent Rust supply-chain checks + +on: + pull_request: + paths: + - ".github/workflows/agent-supply-chain.yml" + - "apps/maple-agent/deny.toml" + - "apps/maple-agent/**/Cargo.toml" + - "apps/maple-agent/Cargo.lock" + - "proxy/Cargo.toml" + - "sdk/rust/Cargo.toml" + push: + branches: [master] + paths: + - ".github/workflows/agent-supply-chain.yml" + - "apps/maple-agent/deny.toml" + - "apps/maple-agent/**/Cargo.toml" + - "apps/maple-agent/Cargo.lock" + - "proxy/Cargo.toml" + - "sdk/rust/Cargo.toml" + schedule: + - cron: "43 7 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + agent-cargo-deny: + name: Agent RustSec advisories and malicious crates + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Check Agent RustSec advisories and incident denylist + uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1; cargo-deny 0.20.2 + with: + # Match the component toolchain for Cargo metadata; no crate is built. + rust-version: 1.98.0 + manifest-path: apps/maple-agent/Cargo.toml + command: check advisories bans + arguments: --config apps/maple-agent/deny.toml --all-features --locked diff --git a/apps/maple-agent/README.md b/apps/maple-agent/README.md index e221af3d3..00e4c6051 100644 --- a/apps/maple-agent/README.md +++ b/apps/maple-agent/README.md @@ -434,6 +434,14 @@ a release build. `just ci` is the full local format, lint, build and test gate; `just release` separately validates the optimized binary. PR jobs have no signing or publishing credentials. See the root agent guide for shared checks. +The independent `.github/workflows/agent-supply-chain.yml` lane checks Agent's +locked dependency graph against current RustSec advisories and the repository's +malicious-crate denylist on dependency changes and daily. It uses the component's +`deny.toml`, scans all features, and has no signing or publishing credentials. +Vulnerabilities, unsoundness and unmaintained-crate advisories block this lane; +no advisory exceptions are inherited from other components. Findings require +review and a separate dependency fix rather than suppression during import. + ## Update and release boundary The launch check accepts only stable `maple-agent-vX.Y.Z` releases and selects diff --git a/apps/maple-agent/deny.toml b/apps/maple-agent/deny.toml new file mode 100644 index 000000000..59d3f8b2e --- /dev/null +++ b/apps/maple-agent/deny.toml @@ -0,0 +1,26 @@ +[advisories] +# Scan the whole Agent graph, including transitive dependencies. Do not inherit +# advisory exceptions from another component or hide new findings at import. +unsound = "all" +unmaintained = "all" + +[bans] +# Emergency supply-chain containment for the August 2026 crates.io campaign. +# Keep these explicit package bans alongside the blocking RustSec checks. +multiple-versions = "allow" +wildcards = "allow" +deny = [ + # Legitimate crates whose malicious releases were removed. Block the + # compromised release and all later releases pending maintainer recovery. + { crate = "arrayref@>=0.3.10", reason = "Compromised release: RUSTSEC-2026-0260" }, + { crate = "append-only-vec@>=0.1.9", reason = "Compromised release: RUSTSEC-2026-0262" }, + { crate = "internment@>=0.8.7", reason = "Compromised release: RUSTSEC-2026-0266" }, + + # Campaign-associated malicious package names. Deny every version. + { crate = "arone", reason = "Malicious crate: RUSTSEC-2026-0259" }, + { crate = "aronenao", reason = "Malicious crate: RUSTSEC-2026-0261" }, + { crate = "tinymember", reason = "Malicious crate: RUSTSEC-2026-0263" }, + { crate = "proc-macro-en", reason = "Malicious crate: RUSTSEC-2026-0264" }, + { crate = "proc-macro1", reason = "Malicious crate: RUSTSEC-2026-0265" }, + { crate = "aovine", reason = "Campaign-associated malicious crate removed from crates.io" }, +] diff --git a/scripts/ci/agent_change_detection.py b/scripts/ci/agent_change_detection.py index 1305e6816..2215d5f22 100644 --- a/scripts/ci/agent_change_detection.py +++ b/scripts/ci/agent_change_detection.py @@ -18,6 +18,7 @@ "README.md", "LICENSE", ".gitignore", + "deny.toml", # Validated by the independent supply-chain lane. } ) AGENT_INERT_PREFIXES = ("docs/",) diff --git a/scripts/ci/test_agent_change_detection.py b/scripts/ci/test_agent_change_detection.py index 51f306ded..a432c2d31 100644 --- a/scripts/ci/test_agent_change_detection.py +++ b/scripts/ci/test_agent_change_detection.py @@ -53,6 +53,7 @@ def test_docs_and_standalone_dependency_inputs_skip_both_apps(self): for path in ( "apps/maple-agent/README.md", "apps/maple-agent/AGENTS.md", "apps/maple-agent/CLAUDE.md", "apps/maple-agent/LICENSE", + "apps/maple-agent/deny.toml", ".github/workflows/agent-supply-chain.yml", "apps/maple-agent/docs/development.md", "README.md", "sdk/rust/README.md", "sdk/rust/tests/client.rs", "sdk/rust/Cargo.lock", "proxy/README.md", "proxy/tests/health.rs", "proxy/Cargo.lock", diff --git a/scripts/ci/test_agent_workflows.py b/scripts/ci/test_agent_workflows.py index 82ab1085e..f8cb5bbc2 100644 --- a/scripts/ci/test_agent_workflows.py +++ b/scripts/ci/test_agent_workflows.py @@ -7,6 +7,7 @@ import shutil import subprocess import tempfile +import tomllib import unittest @@ -110,6 +111,58 @@ def test_namespaced_agent_releases_do_not_enter_research_jobs(self): self.assertIn("github.event.workflow_run.event == 'release'", condition) +class AgentSupplyChainTests(unittest.TestCase): + def test_scan_has_no_credentials_publication_or_privileged_events(self): + config = workflow("agent-supply-chain.yml") + self.assertEqual(set(config["on"]), + {"push", "pull_request", "schedule", "workflow_dispatch"}) + self.assertEqual(config["permissions"], {"contents": "read"}) + for value in strings(config): + self.assertNotRegex(value, r"\bsecrets\b|github\.token|\bGH_TOKEN\b") + self.assertEqual(set(config["jobs"]), {"agent-cargo-deny"}) + job = config["jobs"]["agent-cargo-deny"] + self.assertEqual(job["runs-on"], "ubuntu-latest") + self.assertLessEqual(job["timeout-minutes"], 10) + self.assertNotIn("environment", job) + self.assertNotIn("permissions", job) + self.assertNotIn("continue-on-error", job) + self.assertEqual(len(job["steps"]), 2) + for step in job["steps"]: + self.assertNotIn("run", step) + self.assertNotIn("continue-on-error", step) + self.assertRegex(step["uses"], + r"^(actions/checkout|EmbarkStudios/cargo-deny-action)@[0-9a-f]{40}$") + self.assertIs(job["steps"][0]["with"]["persist-credentials"], False) + scan = job["steps"][1]["with"] + self.assertEqual(scan, { + "rust-version": "1.98.0", + "manifest-path": "apps/maple-agent/Cargo.toml", + "command": "check advisories bans", + "arguments": "--config apps/maple-agent/deny.toml --all-features --locked", + }) + + def test_scan_covers_each_dependency_manifest_and_the_agent_lock(self): + config = workflow("agent-supply-chain.yml") + self.assertEqual(config["on"]["push"]["branches"], ["master"]) + paths = config["on"]["pull_request"]["paths"] + self.assertEqual(paths, config["on"]["push"]["paths"]) + self.assertEqual(set(paths), { + ".github/workflows/agent-supply-chain.yml", + "apps/maple-agent/deny.toml", "apps/maple-agent/**/Cargo.toml", + "apps/maple-agent/Cargo.lock", "proxy/Cargo.toml", "sdk/rust/Cargo.toml", + }) + self.assertEqual(len(config["on"]["schedule"]), 1) + self.assertRegex(config["on"]["schedule"][0]["cron"], r"^\d+ \d+ \* \* \*$") + + def test_agent_policy_does_not_inherit_advisory_exceptions(self): + config = tomllib.loads((ROOT / "apps/maple-agent/deny.toml").read_text()) + self.assertEqual(config["advisories"], {"unsound": "all", "unmaintained": "all"}) + # Incident containment applies to the new component too. An intentional + # policy update must reconcile both lists rather than accidentally omit it. + sdk = tomllib.loads((ROOT / "sdk/deny.toml").read_text()) + self.assertEqual(config["bans"]["deny"], sdk["bans"]["deny"]) + + class AgentDiffSelectionTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory()