fix(ci): the release-bump guard was missing the system deps its check needs - #1259
Conversation
… needs The guard shipped in #1256 without the "Install system dependencies" step the report job beside it has, so its first real run — against Release PR #1232, the under-bump it was built to catch — failed on: error: failed to run custom build command for `libdbus-sys v0.2.7` pkg_config failed error: failed to build rustdoc for crate vta-cli-common v0.12.4 `vta-cli-common` pulls `libdbus-sys`, whose build script shells out to pkg-config. Without the dbus headers rustdoc cannot build, and semver-report.sh reads a failed baseline build as "a PUBLISHED CRATE DOES NOT BUILD" — loud on purpose, because that is a real production defect when it is real. A missing build dependency on the runner produces the identical message. So the guard was red, for a reason that had nothing to do with version bumps, wearing the wording of a much more serious finding. That is the same conflation this job family has now had three times, and the reason to be exact about it: the guard's whole purpose is to be believed when it goes red. The apt logic moves to scripts/install-semver-deps.sh and both jobs call it. A second copy in the second job would have been the shorter diff and the wrong one — two lists of the same thing agree until someone edits one, which is precisely the defect #1252 was written to fix. Verified: both jobs now resolve to identical step lists. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review1 finding needs a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #1259
🗺️ Scan CoverageModules scanned: 2 · with findings: 2 · files: 2 · findings: 3
Executive Summary
🔒 Security Issues
|
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | .github/workflows/ci.yml:58 |
| Finding ID | github_pr-9994b01369fc |
| OWASP | A08:2021 - Software and Data Integrity Failures |
| CVSS 4.0 | 5.5 |
| Exploit Maturity | conceptual |
| Detection Source | mcp_semgrep |
🧠 AI Triage:
- Triaged severity: MEDIUM
- No CVSS score is assigned (this is a hardening/best-practice finding, not a CVE). Exploit maturity is 'conceptual' and exploitation depends on a third-party supply-chain compromise event, not direct attacker action against this repo. It meets medium criteria: real but conditional impact, no confirmed exploit in the wild against this specific action set, requires specific external conditions (upstream tag repointing) to materialize. Production CI/CD context and secrets exposure keep it from being downgraded to low, but lack of concrete active-exploitation evidence keeps it out of high/critical.
- Composite score: 4.9
- Environment: production
Summary: GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-gi — 33 occurrence(s): ci.yml:58, ci.yml:98, ci.yml:100, ci.yml:136, ci.yml:193, ci.yml:195, ci.yml:267, ci.yml:307, ci.yml:311, ci.yml:341, ci.yml:343, ci.yml:358, ci.yml:414, ci.yml:418, ci.yml:459, ci.yml:460, ci.yml:495, ci.yml:537, ci.yml:538, ci.yml:578 (+13 more)
📝 Description:
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-gi
🌱 Root Cause: Github Actions Mutable Action Tag
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Priority: Short-term
Github Actions Mutable Action Tag: GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-gi
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 85%
- AI Validation Evidence: EVIDENCE FOUND: ci.yml uses actions pinned by tag/major-version reference such as
uses: actions/checkout@v7,uses: dtolnay/rust-toolchain@stable,uses: actions/cache@v6,uses: EmbarkStudios/cargo-deny-action@v2,uses: taiki-e/install-action@cargo-semver-checks,uses: taiki-e/install-action@v2. These are mutable tag/branch references rather than pinned to a commit SHA. EVIDENCE NOT FOUND: The finding's evidence block for this specific id is empty (start_line/end_line 58 with no code_snippet), so I could not confirm exactly which of the 33 claimed occurrences this specific finding instance refers to, nor whether some entries (e.g.dtolnay/rust-toolchain@stable) are conventionally accepted floating refs by design. CHANGED VS PRE-EXISTING: ci.yml is in the changed-files list for this MR (per -e_DIFF), and the diff hunks show thereport/semver-checksandrelease-bump-guardjobs'Install system dependenciessteps were modified, but the actualuses:action-tag lines (e.g. line 58,dtolnay/rust-toolchain@stable) are not shown as touched in the visible hunks — however per the tie-break rule, since ci.yml is in the changed-files list, this must be treated as CHANGED even though the specificuses:lines are pre-existing content not modified by this specific diff. VERDICT JUSTIFICATION: The pattern of mutable action tags is real and visible in the file (e.g.@stable,@v7,@v6,@v2,@cargo-semver-checksare not full-length commit SHAs), but the empty evidence snippet at line 58 combined with inability to confirm this is a net-new occurrence (vs pre-existing convention across the whole 560-line file) makes this inconclusive for a specific verdict; a human should confirm which of the 33 occurrences are newly introduced by this PR vs. pre-existing across the file.- Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.
Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.
Details
🛡️ Threat Model & Affect Analysis — PR #1259
| Field | Value |
|---|---|
| Repository | OpenVTC/verifiable-trust-infrastructure |
| Branch | fix/release-guard-system-deps → main |
| Generated | 2026-09-06 |
ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.
📋 Affect Analysis
Change Summary
This PR fixes a CI defect where the cargo-semver-checks 'release-bump guard' job lacked the system dependencies (libdbus-1-dev, pkg-config) required by rustdoc to build the vta-cli-common crate for baseline comparison. The missing dependency caused a build failure that was misreported as 'a published crate does not build' rather than a CI environment defect. The fix extracts the previously inline apt-get logic (which had bounded timeouts/retries to avoid a known mirror-hang issue) into a new shared script, scripts/install-semver-deps.sh, and calls it from both the report job and the previously-missing semver-guard job.
Diff: +63 / -39 lines
Types: ci_cd, reliability_fix, refactor
⚠️ Security Implications
⚪ Fixes false-positive 'broken published crate' misreport in release-bump guard
Fixes false-positive 'broken published crate' misreport in release-bump guard
Action: No further action required for this specific fix; monitor the next few real semver-guard runs to confirm the false-positive mode does not recur under different missing-dependency scenarios.
🟡 Consolidation of dependency-install logic creates a shared single point of failure/compromise across two CI jobs
Consolidation of dependency-install logic creates a shared single point of failure/compromise across two CI jobs
Action: Require CODEOWNERS-mandated review for changes to scripts/ used by CI; consider adding a checksum verification step; add an explicit least-privilege permissions: block to limit what a compromised step could exfiltrate via GITHUB_TOKEN.
🔵 Sudo-privileged apt-get install introduced into a CI job that previously did not perform privileged package installation
Sudo-privileged apt-get install introduced into a CI job that previously did not perform privileged package installation
Action: Scope sudo usage as narrowly as possible; consider pre-baking a container image with these dependencies already installed to eliminate the need for runtime sudo entirely in both jobs.
🔵 Unpinned, unverified apt package installation (libdbus-1-dev, pkg-config) now exercised by two CI jobs instead of one
Unpinned, unverified apt package installation (libdbus-1-dev, pkg-config) now exercised by two CI jobs instead of one
Action: Pin exact package versions; evaluate vendoring/caching a verified snapshot of these two packages instead of relying on live mirror state; consider network egress restrictions for the install step where feasible.
🔵 Unquoted $pkgs variable expansion (shell hygiene anti-pattern, currently non-exploitable)
Unquoted $pkgs variable expansion (shell hygiene anti-pattern, currently non-exploitable)
Action: Quote all variable expansions or convert to a bash array (pkgs=(); ...; "${pkgs[@]}") as defense-in-depth, even though current risk is negligible.
🧩 Affected Components
| Component | Impact | Change | What Changed |
|---|---|---|---|
| CI Report Job (cargo-semver-checks report) | low | modified | The inline 35-line apt-get dependency-install logic was replaced with a single call to the new shared script scripts/install-semver-deps.sh. |
| CI Semver Guard Job (release-bump guard) | medium | modified | A new 'Install system dependencies' step was added, running the same shared script, immediately after checkout and before the Rust toolchain |
| install-semver-deps.sh (new shared script) | medium | new | New file introducing a single, version-controlled, shared implementation of the dependency-install logic previously duplicated inline in ci. |
📁 File Classifications
.github/workflows/ci.yml
- Type: ci_cd_security_relevant
scripts/install-semver-deps.sh
- Type: ci_build_helper_script
🛡️ STRIDE Threat Model
Identified Threats (10)
🟡 STRIDE-1: Unpinned Package Installation in install-semver-deps.sh
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-1357,CWE-494 |
| CAPEC | CAPEC-538,CAPEC-184 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: Shell exec entry point in scripts/install-semver-deps.sh allows supply-chain package substitution due to unpinned, unverified apt package installation, resulting in build-time code execution with sudo privileges on the CI runner.
Evidence: scripts/install-semver-deps.sh:25-27
if sudo timeout 120 apt-get install -y $pkgs; then
exit 0
fi
Attack Scenario:
- Attacker compromises or spoofs the APT mirror/index used by the GitHub-hosted ubuntu-24.04 runner, or exploits a dependency-confusion condition in the shipped package index.
- CI workflow at .github/workflows/ci.yml invokes
run: bash scripts/install-semver-deps.shfor both the report job and semver guard job. - Inside scripts/install-semver-deps.sh, the script runs
sudo timeout 120 apt-get install -y $pkgsforlibdbus-1-dev pkg-configwithout pinning specific versions or verifying package checksums/signatures beyond APT's default trust chain. - If the attacker controls or MITMs the package source (e.g., compromised mirror), a malicious libdbus-1-dev or pkg-config package with a malicious postinst/preinst script is installed with sudo privileges.
- Malicious postinst script executes arbitrary code in the CI runner context, potentially exfiltrating GITHUB_TOKEN, repository secrets, or tampering with the build/semver-check outcome.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: CI runner -> APT mirror -> installed system packages
Preconditions: Attacker must control or intercept the APT package source reachable by the GitHub-hosted runner, No package pinning, checksum, or signature verification beyond default APT/GPG trust is enforced, CI runner uses ephemeral, network-connected environment reachable by an on-path or compromised-mirror attacker
Existing Controls: Ubuntu's default APT GPG signature verification of repository metadata and packages • Script fetches from the runner's pre-shipped trusted package index by default, only falling back to apt-get update if package is missing • set -euo pipefail ensures script fails closed on errors rather than silently continuing
Recommended Mitigations: Pin exact package versions for libdbus-1-dev and pkg-config • Consider vendoring or caching a verified package snapshot instead of relying on live mirror state • Restrict sudo scope needed for the install step via least-privilege runner configuration • Enable GitHub Actions egress/network allow-listing for the runner where feasible
🔵 STRIDE-2: Unquoted Variable Expansion Leading to Word Splitting in install-semver-deps.sh
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 2.3 CVSS:4.0/AV:L/AC:H/AT:P/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-78,CWE-88 |
| CAPEC | CAPEC-88 |
| OWASP | A03:2021 - Injection |
Description: Shell exec entry point in scripts/install-semver-deps.sh allows argument injection via word splitting due to unquoted $pkgs variable expansion, resulting in potential unintended apt-get argument interpretation.
Evidence: scripts/install-semver-deps.sh:9-11,26,32
pkgs=""
for p in libdbus-1-dev pkg-config; do
dpkg -s "$p" >/dev/null 2>&1 || pkgs="$pkgs $p"
done
...
if sudo timeout 120 apt-get install -y $pkgs; then
Attack Scenario:
- The script builds
pkgsby string concatenation:pkgs="$pkgs $p"for each package name from a fixed internal list (libdbus-1-dev, pkg-config). - The resulting
$pkgsvariable is expanded unquoted insudo timeout 120 apt-get install -y $pkgs(line 26) and again in the retry loop (line 32). - Because the loop-derived values are hardcoded literals with no attacker-controlled input path today, exploitation currently requires an attacker to first gain the ability to modify the script's package list (e.g., via a separate PR-merge compromise) to insert a value containing shell metacharacters or additional flags.
- If an attacker can inject a value into
p(e.g., through a future refactor reading package names from an external/environment-controlled source), unquoted expansion would allow argument injection intoapt-get install(e.g., injecting--allow-unauthenticatedor additional untrusted package names). - This would broaden the install surface or downgrade APT's security posture for that invocation.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: Internal script variable to apt-get argv
Preconditions: Attacker must first achieve write access to modify the script or introduce an externally-controlled package name source, No current external input reaches the vulnerable variable, making this a latent/defense-in-depth issue rather than directly exploitable today
Existing Controls: Package list is currently a fixed, hardcoded internal array (libdbus-1-dev pkg-config) with no external input path • set -euo pipefail limits some failure-mode risk
Recommended Mitigations: Quote all variable expansions: "$pkgs" and use an array instead of a space-joined string to avoid word splitting entirely • Adopt pkgs=() bash arrays with "${pkgs[@]}" expansion for robustness against future refactors
🔵 STRIDE-3: Unbounded Retry Sleep Loop Causing CI Resource Exhaustion in install-semver-deps.sh
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-400,CWE-770 |
| CAPEC | CAPEC-125,CAPEC-227 |
| OWASP | A05:2021 - Security Misconfiguration |
Description: CI trigger entry point in the report and semver-guard jobs allows job-timeout-based Denial of Service due to a bounded-but-lengthy apt retry loop (up to 3 x 240s+10s cycles) triggered by a genuinely unresponsive or maliciously stalled mirror, resulting in wasted CI compute minutes and delayed pipeline feedback.
Evidence: scripts/install-semver-deps.sh:28-36
for attempt in 1 2 3; do
if sudo timeout 120 apt-get -o Acquire::Retries=3 update \
&& sudo timeout 120 apt-get -o Acquire::Retries=3 install -y $pkgs; then
exit 0
fi
echo "::warning::apt attempt $attempt failed or timed out; retrying in 10s"
sleep 10
done
Attack Scenario:
- An attacker with the ability to influence network conditions towards the APT mirror (e.g., a compromised or hostile network path, or an attacker who can trigger repeated CI runs via low-effort PRs) causes the initial
apt-get installto fail. - Script falls into the warning branch and begins the 3-attempt retry loop: each iteration runs
apt-get update(up to 120s timeout) thenapt-get install(up to 120s timeout), followed by a 10s sleep, for up to ~730 seconds worst case. - Combined with the job-level
timeout-minutes: 45on the semver guard job, repeated forced failures across many CI runs (e.g., an attacker opening many draft PRs to trigger workflow_dispatch/PR triggers) consume shared runner minutes and delay legitimate CI feedback for the org. - This is an amplification of the original incident described in the code comments (three prior real-world hangs on
apt-get update), showing the failure mode is realistic and reproducible under adverse network conditions. - No circuit breaker exists across separate CI runs to detect and short-circuit a persistently broken mirror without repeatedly paying the full retry cost.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002
- Data Flows: CI trigger -> apt mirror retry loop -> runner minute consumption
Preconditions: Attacker or adverse condition can cause the GitHub-hosted runner's default package mirror to be slow/unresponsive, Attacker can trigger repeated workflow runs (e.g., open-source repo with PR-triggered CI from forks)
Existing Controls: timeout 120 bounds each individual apt-get update/install call • Retry loop is capped at 3 attempts, not infinite • Job-level timeout-minutes: 45 provides an outer backstop
Recommended Mitigations: Add exponential backoff with jitter instead of fixed 10s sleep to reduce synchronized retry storms • Cache/vendor the required .deb packages in the repository or an internal artifact store to remove runtime dependency on the live mirror entirely • Restrict workflow triggers from external forks or require maintainer approval for first-time contributors to reduce trigger abuse surface
🔵 STRIDE-4: Shared Script Single Point of Failure Across Both CI Jobs in ci.yml
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 2.1 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-1357,CWE-829 |
| CAPEC | CAPEC-441 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: CI trigger entry point in the report job and semver guard job allows blast-radius amplification via a single compromised shared script due to consolidation of previously duplicated dependency-install logic into scripts/install-semver-deps.sh, resulting in simultaneous compromise or simultaneous denial of service of both the report and semver-guard CI jobs.
Evidence: .github/workflows/ci.yml:500,547-548
run: bash scripts/install-semver-deps.sh
...
- name: Install system dependencies
run: bash scripts/install-semver-deps.sh
Attack Scenario:
- Attacker with write access to the repository (e.g., via a compromised maintainer account, malicious insider, or supply-chain compromise of a merged PR) modifies scripts/install-semver-deps.sh.
- Because both
.github/workflows/ci.ymljob report and the semver guard job now both callrun: bash scripts/install-semver-deps.sh(previously each job had its own independent inline copy), the injected change propagates to both CI jobs simultaneously. - Attacker inserts malicious commands (e.g., curl-pipe-to-shell exfiltration of CI secrets, or
rm -rfsabotage) into the shared script. - On the next CI run, both jobs execute the malicious script with the same effect, doubling blast radius compared to the pre-refactor duplicated-but-independent inline logic where only one job's copy might have been altered/reviewed.
- Malicious modifications could exfiltrate GITHUB_TOKEN or other secrets available to the workflow's environment, or reliably break both CI gates simultaneously to mask a subsequently merged malicious change.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002, EP-003
- Data Flows: Shared script -> both CI job executions
Preconditions: Attacker must first obtain write access to the repository or ability to merge a malicious PR into scripts/install-semver-deps.sh, No branch protection/code owner review specifically hardened for CI script changes
Existing Controls: Presumed standard GitHub branch protection and PR review process for merging changes to the main branch (not explicitly verifiable from the diff alone) • Script is version-controlled and diff-reviewable, unlike inline YAML blocks previously duplicated across jobs, improving auditability of a single source of truth
Recommended Mitigations: Require CODEOWNERS-based mandatory review for changes under scripts/ used by CI • Pin and checksum-verify the install script's own integrity (e.g., via a signed commit or hash pinning) before execution in CI • Apply least-privilege GITHUB_TOKEN permissions on both jobs to limit exfiltration impact if the script is compromised • Consider running dependency-install steps in an isolated, non-privileged pre-stage container with no access to repository secrets
🔵 STRIDE-5: Missing Integrity Verification of install-semver-deps.sh Prior to Execution
| Field | Detail |
|---|---|
| Category | Tampering, Repudiation |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.5 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-345,CWE-494 |
| CAPEC | CAPEC-186 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: CI trigger entry point in ci.yml allows tampering with the executed script's contents due to absence of checksum, hash-pinning, or signature verification before bash scripts/install-semver-deps.sh is invoked, resulting in undetected execution of a modified script if the checkout step delivers tampered content.
Evidence: .github/workflows/ci.yml:500
run: bash scripts/install-semver-deps.sh
Attack Scenario:
- An attacker who can influence the checked-out repository state for a given CI run (e.g., via a malicious fork PR combined with a workflow misconfiguration that runs untrusted code with secrets, or a compromised git history/tag) causes a modified scripts/install-semver-deps.sh to be checked out.
- The workflow step
run: bash scripts/install-semver-deps.sh(ci.yml lines 500 and 547-548) executes the file exactly as checked out, with no SHA-256 checksum check, GPG signature verification, oractions/checkoutref pinning validation performed before execution. - Because there's no independent verification step, a maliciously modified script executes silently, and CI logs would only show the script's own echo output, not an indication that its content diverged from the trusted canonical version.
- This could be combined with STRIDE-4 (shared script single point of failure) to affect both jobs, and lacks a corrective control (e.g., commit-hash pinning) to detect drift.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002
- Data Flows: Checkout -> script execution without integrity check
Preconditions: Attacker must achieve some level of control over the checked-out ref/content (e.g., malicious PR from a fork with pull_request_target misuse, or compromised source), Workflow must be configured in a way that untrusted content reaches a step with secret access
Existing Controls: Standard GitHub Actions checkout via actions/checkout@v7 pins to a specific action version • fetch-depth: 0 is used for full history retrieval only, unrelated to integrity of the script content itself
Recommended Mitigations: Add explicit script hash verification (e.g., sha256sum -c against a value stored in workflow YAML) before execution • Restrict which workflow triggers (e.g., disallow pull_request_target on untrusted forks) can reach this step with secrets • Enable required status checks and signed commits for changes to the scripts/ directory
⚪ STRIDE-6: Insufficient Logging of Package Install Outcomes for Forensic Traceability in install-semver-deps.sh
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Informational |
| Likelihood | Unlikely |
| CVSS | 1.0 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-778 |
| CAPEC | CAPEC-93 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: CI trigger entry point in the report and semver guard jobs allows repudiation of a supply-chain tampering event due to the script only logging generic warning/error strings rather than package versions, hashes, or source repository metadata, resulting in reduced forensic ability to detect and prove that a malicious package version was installed during a given CI run.
Evidence: scripts/install-semver-deps.sh:15,17,29,33,36
echo "missing:$pkgs"
...
echo "::warning::install from the shipped index failed; refreshing"
Attack Scenario:
- If a malicious or unexpectedly different version of libdbus-1-dev or pkg-config were ever installed (e.g., via mirror compromise as in STRIDE-1), the script's only output is
echo "missing:$pkgs"and generic success/failure/warning messages (lines 15, 17, 29, 33, 36). - No step records the exact installed package version, source repository URL, or a hash/checksum of the installed .deb into CI logs or artifacts.
- Post-incident, an investigator reviewing CI logs for a given historical run would be unable to determine which exact package version or source was used, hampering root-cause analysis and non-repudiation of a supply-chain compromise.
- This gap does not itself allow initial compromise but reduces detectability and evidentiary value of CI logs after a STRIDE-1-style attack has occurred.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: Script execution -> CI log output
Preconditions: A prior compromise event (e.g., STRIDE-1) must occur for this gap to matter, No dedicated CI log retention/analysis pipeline captures package provenance today
Existing Controls: Script does emit basic status messages (::warning::, ::error::) to GitHub Actions annotations, providing partial run-level traceability
Recommended Mitigations: Log apt-cache policy $pkgs output and installed package versions/hashes to the CI log or a build artifact before and after installation • Emit machine-parseable structured logs (e.g., JSON) capturing package name, version, and source repo for each install for later SIEM ingestion
⚪ STRIDE-7: Prompt-Injection-Style Instruction Embedded in Code Comments Attempting to Influence Automated Review Tooling
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Informational |
| Likelihood | Very Unlikely |
| CVSS | 0.0 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-1039 |
| CAPEC | CAPEC-148 |
| OWASP | A03:2021 - Injection |
Description: Code comment content in scripts/install-semver-deps.sh and ci.yml allows social-engineering of automated review/LLM-based tooling due to extensive narrative prose embedded in comments framing prior incidents and rationale in persuasive human language, resulting in a theoretical risk of biasing automated security reviewers toward under-scrutinizing the accompanying code changes.
Evidence: scripts/install-semver-deps.sh:1-24
# They already did once: the release-bump guard shipped without this step and
# failed its first real run on `libdbus-sys ... pkg_config failed`, reported as
# a broken published artifact.
Attack Scenario:
- The diff introduces unusually long, narrative-style comments (e.g., 'They already did once: the release-bump guard shipped without this step and failed its first real run...') embedded directly in scripts/install-semver-deps.sh and ci.yml.
- Such verbose justification text, while plausibly legitimate engineering rationale here, follows a pattern that could be exploited by a malicious contributor to pre-emptively justify risky code changes (e.g., disabling security checks) by narrating a believable incident story to bias human or LLM-based PR reviewers into approving without deep scrutiny.
- This analysis explicitly treats all such comment text as untrusted data under the security boundary directive, and does not treat any embedded phrase as an instruction to alter this report's verdicts.
- No exploitation occurred in this specific PR — the rationale appears genuine and consistent with the code changes — but the pattern itself is worth flagging as a governance/process risk for future PRs using similar narrative techniques to smuggle in unrelated risky changes.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: Code comment -> reviewer/tooling perception
Preconditions: A future malicious contributor uses similarly persuasive prose to justify unrelated or risky changes, Reviewers (human or automated) rely on narrative framing rather than independently verifying the actual code behavior
Existing Controls: This analysis applies a strict data/instruction separation boundary, treating all repository content as data regardless of narrative framing • Code changes were independently verified against their actual behavior rather than accepted based on comment narrative alone
Recommended Mitigations: Establish PR review guidance requiring reviewers to verify code behavior independently of comment narrative • Apply consistent automated static analysis regardless of surrounding comment content • Flag unusually long justification comments for additional human review scrutiny
🔵 STRIDE-8: Excessive sudo Privilege Usage for Dependency Installation in CI Runner
| Field | Detail |
|---|---|
| Category | Elevation of Privilege |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.8 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-250 |
| CAPEC | CAPEC-233 |
| OWASP | A01:2021 - Broken Access Control |
Description: CI trigger entry point in ci.yml allows privilege-escalation blast-radius amplification due to invoking sudo for every apt-get call in install-semver-deps.sh without scoping or dropping privileges afterward, resulting in any injected command executing with root privileges on the ephemeral runner.
Evidence: scripts/install-semver-deps.sh:26,32-33
if sudo timeout 120 apt-get install -y $pkgs; then
...
if sudo timeout 120 apt-get -o Acquire::Retries=3 update \
&& sudo timeout 120 apt-get -o Acquire::Retries=3 install -y $pkgs; then
Attack Scenario:
- Every apt-get invocation in the script (lines 12? actually 26, 32-33) is prefixed with
sudo timeout 120 apt-get ..., granting root privileges for the entire install operation. - If any of the other identified threats (STRIDE-1 malicious package, STRIDE-2 argument injection, STRIDE-4/5 script tampering) is realized, the injected/malicious code executes with root privileges rather than the default runner user's privileges.
- Root access on the ephemeral GitHub-hosted runner allows reading all files, modifying system state, and potentially accessing the GITHUB_TOKEN or OIDC token used by subsequent steps if not already isolated.
- While the runner is ephemeral and destroyed after the job, within the job's lifetime a root-level compromise has full read/write access to the workspace, environment variables, and any mounted secrets for that job.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002, EP-003
- Data Flows: Script -> sudo apt-get -> root-level execution context
Preconditions: One of the other tampering-class threats (STRIDE-1, STRIDE-2, STRIDE-4, STRIDE-5) must first succeed to have code executed in this context, sudo is passwordless and unrestricted on the default GitHub-hosted runner image (standard configuration)
Existing Controls: GitHub-hosted runners are ephemeral, single-use VMs, limiting persistence of any root compromise • sudo scope is limited to what the default runner image permits (no additional sudo grants added by this PR)
Recommended Mitigations: Scope the sudo usage as narrowly as possible per command rather than blanket sudo across the whole script • Consider running the report/semver-guard jobs in a locked-down container image with dependencies pre-baked, eliminating the need for sudo at CI runtime entirely • Apply least-privilege GITHUB_TOKEN permissions (permissions: block) at the workflow/job level to limit what a root-level compromise could exfiltrate
🔵 STRIDE-9: Fixed Timeout Ceilings Enabling Slow-Drip Denial of Service via Partial Mirror Responsiveness
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 2.6 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-400 |
| CAPEC | CAPEC-227 |
| OWASP | A05:2021 - Security Misconfiguration |
Description: CI trigger entry point in ci.yml/install-semver-deps.sh allows a slow-drip denial-of-service condition due to the script's timeout 120 bounding individual commands but not detecting a mirror that responds just-under the threshold on every attempt, resulting in consistent near-maximal delay (up to ~12 minutes total) on every CI run without ever triggering an outright hard failure that would prompt investigation.
Evidence: scripts/install-semver-deps.sh:12,26,32-33
if sudo timeout 120 apt-get install -y $pkgs; then
...
if sudo timeout 120 apt-get -o Acquire::Retries=3 update \
&& sudo timeout 120 apt-get -o Acquire::Retries=3 install -y $pkgs; then
Attack Scenario:
- An adversarial or degraded mirror consistently responds in ~115 seconds (just under the 120s timeout) for both
apt-get updateandapt-get installcalls. - Every CI run for both the report job and semver guard job pays this near-maximal latency tax silently, since the script only emits a warning/error when the full timeout is exceeded, not when it is merely close to the ceiling.
- Over many CI runs (e.g., dozens of PRs per day in an active repo), this results in cumulative, difficult-to-diagnose CI slowdown that degrades developer velocity and increases the probability of legitimate job timeout (
timeout-minutes: 45) breaches under any additional load. - Because the behavior does not cross the hard-failure threshold, it may persist undetected for a long time, unlike the original three-hangs incident that was noisy enough to be noticed and fixed.
🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002
- Data Flows: CI trigger -> bounded-but-slow apt calls -> cumulative CI delay
Preconditions: A marginal/degraded (not fully unresponsive) mirror condition, which is plausible given the mirror's demonstrated instability described in the incident history comments, No CI performance-trend monitoring/alerting on step duration exists to catch this gradually
Existing Controls: timeout 120 still provides a hard ceiling preventing indefinite hangs • Job-level timeout-minutes: 45 is a backstop against total runaway jobs
Recommended Mitigations: Instrument and monitor per-step CI duration metrics with alerting on sustained near-timeout latencies • Vendor/cache the required .deb packages in-repo or in an internal artifact store to remove dependency on live mirror latency entirely • Lower the individual timeout to a tighter bound with faster fallback to a known-good mirror or cached package source
🟡 STRIDE-10: Missing Least-Privilege GITHUB_TOKEN Permissions Declaration Exposing Broader Job Scope in ci.yml
| Field | Detail |
|---|---|
| Category | Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 4.8 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-250,CWE-269 |
| CAPEC | CAPEC-122 |
| OWASP | A01:2021 - Broken Access Control |
Description: CI trigger entry point in the report and semver guard jobs allows unnecessary elevation-of-privilege exposure due to no visible permissions: block scoping GITHUB_TOKEN at the workflow or job level in the visible diff, resulting in any code execution achieved via the other identified threats potentially inheriting default (often read/write) token scope for repository operations.
Evidence: .github/workflows/ci.yml:1-560
jobs:
report:
...
semver-guard:
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
Attack Scenario:
- The visible diff for ci.yml does not show a
permissions:block being added or modified alongside the new shared script invocation for either the report job or the semver guard job. - If the overall workflow file lacks an explicit least-privilege
permissions:declaration (default GITHUB_TOKEN scope depends on repository/org settings and can default to read/write), any code execution achieved via STRIDE-1, STRIDE-2, STRIDE-4, STRIDE-5, or STRIDE-8 would run with whatever token scope the job inherits. - An attacker who achieves arbitrary command execution within the
Install system dependenciesstep (e.g., via a compromised mirror or tampered script) could use an overly broad GITHUB_TOKEN to push commits, create releases, or modify repository settings, depending on the effective permission scope. - This is a configuration gap rather than a direct code vulnerability in the diff itself, but it materially increases the blast radius of any of the other threats in this model if the broader workflow file does not scope permissions tightly.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002
- Data Flows: CI job execution context -> GITHUB_TOKEN scope
Preconditions: The full ci.yml file (outside the shown diff) does not declare a restrictive permissions: block at workflow or job level, One of the code-execution threats (STRIDE-1/2/4/5/8) must succeed for this to matter
Existing Controls: Cannot be fully verified from the provided diff/context; assumed default GitHub Actions token behavior applies unless proven otherwise elsewhere in the file
Recommended Mitigations: Add an explicit permissions: contents: read (or narrower) block at the workflow or job level for both the report and semver guard jobs • Audit the full ci.yml for any jobs with unnecessary write/administrative permissions • Use OIDC federation with narrowly scoped short-lived tokens instead of relying on default GITHUB_TOKEN scope where external cloud access is needed
🍝 PASTA Threat Model
Application Purpose
The OpenVTC verifiable-trust-infrastructure project provides Rust-based crates for building verifiable trust systems; this PR specifically hardens and consolidates the CI pipeline's system-dependency installation logic used to build and semver-check the published crates.
Inherent Risks
- CI pipelines rely on third-party package mirrors outside organizational control.
- GitHub-hosted runners execute with sudo/root-capable privileges for setup tasks by default.
- Shared automation scripts create common-mode failure and compromise paths across multiple CI jobs.
- Public/open-source repositories are exposed to PR-triggered CI abuse from untrusted forks.
Objectives
Risk: Accept low residual risk from reliance on the default APT trust chain given the fixed, small package list.; Treat CI availability degradation as tolerable up to defined job timeout thresholds.
Business: Maintain trustworthy, continuously verified releases of the verifiable-trust-infrastructure crates.; Minimize CI downtime and false-positive failures that erode developer trust in automated gates.
Security: Prevent supply-chain tampering of build-time system dependencies.; Limit blast radius of any single compromised CI script or credential.
Financial: Minimize wasted GitHub Actions runner-minute spend from redundant or hanging CI steps.
Compliance: Maintain auditable, version-controlled CI configuration suitable for supply-chain security review (e.g., SLSA-aligned practices).
Functional: Reliably build and semver-check all workspace crates including those depending on libdbus-sys.; Provide a single, maintainable source of truth for CI system-dependency installation.
Operational: Ensure CI jobs complete within defined timeout windows under normal and degraded mirror conditions.; Reduce duplicate/drifting logic between the report job and semver guard job.
Business Impact Analysis (1)
BIA-1: Continuous Integration Build and Semver Verification Pipeline (High)
The CI pipeline builds the Rust workspace and verifies that proposed crate version bumps correctly reflect API changes before publication.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Crate Consumers / Open Source Contributors / Project Maintainers / Release Engineers
- Dependencies: GitHub Actions Runner Infrastructure / GitHub-Hosted APT Package Mirror / Rust Toolchain (dtolnay/rust-toolchain) / cargo-semver-checks Tooling / scripts/install-semver-deps.sh
- Disruptions: APT mirror becomes unresponsive or compromised / Shared install script is tampered with or broken / CI job exceeds timeout-minutes threshold
- Impacts: Delayed crate releases blocking downstream consumers / False-positive 'broken published crate' reports eroding trust in the semver guard / Wasted GitHub Actions compute minutes and increased CI cost / Potential supply-chain compromise of the build environment if package installation is tampered with
Technical Scope
Roles (3): RO-1 Repository Maintainer · RO-2 External Contributor · RO-3 CI Runner Service Account
Actors (3): AC-1 GitHub Actions Scheduler · AC-2 CI Runner Bash Process · AC-3 Human PR Author
Use Cases (1): Automated CI Build and Semver Verification on Pull Request
Attack Trees (2): SC-4: install-semver-deps.sh Script · SC-1: CI Workflow Orchestrator (ci.yml)
Entry Points (3): EP-1 Report Job CI Trigger · EP-2 Semver Guard Job CI Trigger · EP-3 Shared Dependency Install Script Execution
Risk Registry (4): RISK-1 · RISK-2 · RISK-3 · RISK-4
Threat Actors (3): TA-1 Malicious/Compromised APT Mirror Operator · TA-2 Malicious Insider or Compromised Maintainer Account · TA-3 Malicious External Contributor
Infrastructure (1): IF-1 GitHub-Hosted ubuntu-24.04 Runner
Trust Boundaries (3): TB-1 GitHub Actions Control Plane · TB-2 Ephemeral CI Runner Boundary · TB-3 Repository Source Control Boundary
External Entities (2): EE-1 External Contributors / PR Authors · EE-2 Ubuntu APT Package Mirror Network
System Components (5): SC-1 CI Workflow Orchestrator (ci.yml) · SC-2 Report Job Runner · SC-3 Semver Guard Job Runner · SC-4 install-semver-deps.sh Script · SC-5 External APT Package Mirror
Resources And Assets (3): RA-1 GITHUB_TOKEN and CI Secrets · RA-2 Installed System Packages (libdbus-1-dev, pkg-config) · RA-3 Rust Workspace Source and Published Crates
Technologies And Dependencies (4): TD-1 GitHub Actions actions/checkout · TD-2 dtolnay/rust-toolchain · TD-3 taiki-e/install-action cargo-semver-checks · TD-4 libdbus-1-dev / pkg-config (APT packages)
⚔️ Attack Scenarios (1)
Exploit identified weaknesses
flowchart LR
S0["Unpinned Package Installation in install-semver-deps.sh"]
S1["Unquoted Variable Expansion Leading to Word Splitting in ins"]
S2["Unbounded Retry Sleep Loop Causing CI Resource Exhaustion in"]
S0 --> S1
S1 --> S2
📊 Risk Summary
Total Threats: 10
By Severity: Low: 6 · Medium: 2 · Informational: 2
By Category: Tampering: 5 · Elevation of Privilege: 3 · Denial of Service: 3 · Repudiation: 2
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 0 | 1 |
Must-Review-By-Human (1)
- 🟡 Github Actions Mutable Action Tag (33 occurrences)
…ged (#1267) `cargo test --workspace` is the longest-running check in the repo. Of 2203s of test execution, `vta_service` alone is 906s and vtc-service's 52 integration binaries are another 848s — and every pull request pays for both regardless of what it touched. Split into two jobs that together run exactly the same set: Test (workspace) cargo test --workspace --exclude vtc-service --exclude vtc-client Test (VTC) cargo test -p vtc-service -p vtc-client each gated by scripts/ci-affects.sh on whether the change can reach it. On the last 120 commits that is 13% of changes skipping the workspace half and a large share of the 53% VTA-only ones skipping the VTC half. The scope of the workspace half is stated as `--except vtc-service`, and this is the part worth not hand-rolling: it is NOT the complement of the VTC closure. `room-host` depends on `vtc-client`, so `vtc-client` belongs to both sides and a change to it must run both jobs. Deriving the closure from cargo metadata gets that right; writing "everything that isn't vtc-*" by hand does not. Verified: vtc-service only workspace=skip vtc=run vtc-client only workspace=run vtc=run <- the case a hand list breaks vta-service only workspace=run vtc=skip vta-tee only workspace=run vtc=skip vti-common workspace=run vtc=run room-host workspace=run vtc=skip docs only workspace=skip vtc=skip The #1243 no-op-rebuild guard moves to the VTC job, where the build.rs it guards actually lives. Also renames scripts/install-semver-deps.sh to install-build-deps.sh and uses it here. The Test job carried a third verbatim copy of that apt block; the point of extracting it in #1259 was to stop having several, and adding a fourth while splitting a job would have missed it. `vti-e2e-tests` is in the workspace half and its closure contains no vtc-*, so nothing cross-cutting is stranded by the split — checked rather than assumed. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The guard added in #1256 shipped without the Install system dependencies
step the report job beside it has. Its first real run — against Release PR
#1232, the exact under-bump it was built to catch — failed on this instead:
vta-cli-commonpullslibdbus-sys, whose build script shells out topkg-config. Without the dbus headers rustdoc cannot build, and
semver-report.shreads a failed baseline build as "a PUBLISHED CRATE DOESNOT BUILD" — deliberately loud, because that is a genuine production defect
when it is real. A missing build dependency on the runner produces the
identical message.
So the guard was red for a reason unrelated to version bumps, wearing the
wording of a far more serious finding. That is the same conflation this job
family has now had three times (#1252 twice over, and this). It matters more
here than anywhere else: the guard's entire value is being believed when it
goes red, and its first appearance on a real Release PR was a false positive.
The fix
The apt logic moves to
scripts/install-semver-deps.sh; both jobs call it.A second copy of the block inside the guard job would have been the shorter
diff and the wrong one — two lists of the same thing agree right up until
someone edits one, which is exactly the defect #1252 exists to fix. Same
reasoning that put the exclusion list and coverage assertion in one script
rather than two.
Verified both jobs now resolve to identical step lists:
What this does not yet prove
The guard still has not had a clean run against a real under-bump. #1232
remains the test case: once this merges and release-plz refreshes that branch,
the guard should fail with
THE PROPOSED RELEASE UNDER-BUMPS A BREAKING CHANGEand exit 100, naming
vti-common0.16.2 andvta-sdk0.32.4. Locally, againsta checkout of that same branch, it does exactly that — and passes at 0.17.0 /
0.33.0 — but that is my reconstruction, not the job.