Skip to content

ci: split Test into workspace and VTC halves, each gated on what changed - #1267

Merged
stormer78 merged 1 commit into
mainfrom
ci/split-test-job
Sep 6, 2026
Merged

ci: split Test into workspace and VTC halves, each gated on what changed#1267
stormer78 merged 1 commit into
mainfrom
ci/split-test-job

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Follow-up to #1265, extending the same mechanism to the longest 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 PR pays for both regardless
of what it touched.

The split

Two jobs that together run exactly the same set as before:

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.

The part worth not hand-rolling

The workspace half's scope is --except vtc-service, and that is not the
complement of the VTC closure. room-host depends on vtc-client, so
vtc-client belongs to both sides — a change to it must run both jobs. Derived
from cargo metadata this falls out correctly; written by hand as "everything that
isn't vtc-*" it is wrong in a way that skips real coverage.

Verified in a scratch clone:

Change Test (workspace) Test (VTC)
vtc-service only skip run
vtc-client only run run
vta-service only run skip
vta-tee only run skip
vti-common run run
room-host run skip
docs only skip skip

On the last 120 commits that is 13% skipping the workspace half, and a large
share of the 53% VTA-only changes skipping the VTC half.

Also

Safety

Unchanged from #1265: every uncertainty resolves to run (not a PR, lockfile or
.github/ or scripts/ touched, unattributable path, no usable diff), and the
conditions are != 'false' so a missing output runs rather than skips.

Worth watching on the first few PRs: the affects job prints its reasoning and
writes it to the step summary, so a wrong skip should be visible rather than
silent.

`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>
@stormer78
stormer78 requested a review from a team as a code owner September 6, 2026 18:34
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

1 AI-confirmed issue.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #1267

Field Value
Repository OpenVTC/verifiable-trust-infrastructure
Branch ci/split-test-jobmain
Validated 2026-09-06
Scan ID 808cd0ba
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 2 · with findings: 1 · files: 3 · findings: 1

Module Files scanned Findings
scripts 2 0
.github 1 1

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 1 0

🔒 Security Issues

Confirmed Vulnerabilities (1)

🟡 Github Actions Mutable Action Tag (35 occurrences)

Field Detail
Severity MEDIUM
Location .github/workflows/ci.yml:58
Finding ID github_pr-05a507a98f84
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
  • CVSS is not applicable to this CI configuration weakness; exploit maturity is 'conceptual' and exploitation depends on a third party (upstream action maintainer/account) being compromised first — not attacker-controlled directly. This matches medium calibration: real but conditional risk, no confirmed exploit in the wild against this specific repo, though the historical precedent (tj-actions, reviewdog, trivy-action) shows this class of attack is real and has happened.
  • 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 — 35 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:501, ci.yml:502, ci.yml:542 (+15 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: ✅ Confirmed True Positive
  • Confidence: 85%
  • AI Validation Evidence: EVIDENCE FOUND: ci.yml contains many uses: steps referencing actions by mutable tags/branches, e.g. - uses: dtolnay/rust-toolchain@stable (repeated at lines ~199, ~527, ~575, etc.), - uses: dtolnay/rust-toolchain@master (msrv job), - uses: taiki-e/install-action@cargo-semver-checks, - uses: taiki-e/install-action@v2, - uses: actions/checkout@v7, - uses: actions/cache@v6, - uses: EmbarkStudios/cargo-deny-action@v2. None of these are pinned to an immutable commit SHA. EVIDENCE NOT FOUND: No SHA-pinning anywhere in ci.yml; no Dependabot/renovate config or OpenSSF Scorecard step found in provided files that would mitigate mutable-tag risk. CHANGED VS PRE-EXISTING: .github/workflows/ci.yml is explicitly listed in the authoritative changed-files list for this MR, and the diff hunks show modifications to the test/test-vtc jobs which retain uses: dtolnay/rust-toolchain@stable and actions/cache@v6 steps immediately adjacent to the changed lines (e.g. new test-vtc job block added by this diff includes - uses: dtolnay/rust-toolchain@stable and - uses: actions/cache@v6), so the vulnerable pattern is directly introduced/duplicated by this MR, making it CHANGED rather than purely pre-existing. VERDICT JUSTIFICATION: The finding is a real, verifiable code pattern (mutable action references) present in the file and reproduced across newly-added job steps in this PR, satisfying evidence-based confirmation of the construct itself; this is a legitimate supply-chain hardening finding, not a false assumption.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


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.

Complementary: 🛡️ **Threat Model & Affect Analysis**
Details

🛡️ Threat Model & Affect Analysis — PR #1267

Field Value
Repository OpenVTC/verifiable-trust-infrastructure
Branch ci/split-test-jobmain
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 splits the monolithic 'Test' CI job into two parallel jobs ('test' for workspace-minus-VTC, 'test-vtc' for vtc-service/vtc-client), each independently gated on whether the change actually affects that partition, via new 'vta_tests'/'vtc_tests' outputs on the 'affects' job. It also extends ci-affects.sh/ci-closure.py with an '--except' inversion mode to correctly compute 'everything except X' as a derived closure (not a hand-maintained complement), replaces a fragile inline apt-get retry block with a shared install-build-deps.sh script (renamed from install-semver-deps.sh), and adds a disk-space-reclamation step to the VTC job to avoid 'No space left on device' failures when linking the large vtc-service binary.

Diff: +165 / -55 lines
Types: ci_cd, refactor, reliability

📁 File Classifications

.github/workflows/ci.yml

  • Type: ci_config

scripts/ci-affects.sh

  • Type: ci_cd_helper_script

scripts/ci-closure.py

  • Type: ci_cd_helper_script

scripts/install-build-deps.sh

  • Type: build_script

🛡️ STRIDE Threat Model

Identified Threats (12)

🟡 STRIDE-1: Command Injection via Unquoted Variable Expansion in ci-affects.sh Closure Invocation

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.3 CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-88,CWE-78
CAPEC CAPEC-88
OWASP A03:2021 - Injection

Description: SHELL_INVOCATION entry point in scripts/ci-affects.sh allows argument-injection into the downstream python3 subprocess due to unquoted $EXCEPT expansion in the command line, resulting in unintended flag/argument smuggling into ci-closure.py

Evidence: scripts/ci-affects.sh:119-122

# shellcheck disable=SC2086  # $EXCEPT is an intentional bare flag or empty
if ! CLOSURE=$(cargo metadata --format-version 1 2>/dev/null | python3 scripts/ci-closure.py $EXCEPT "$PKG"); then

Attack Scenario:

  1. Attacker with ability to influence workflow-level env vars or a malicious fork's workflow_call parameters controls a value that reaches ci-affects.sh as the first positional token.
  2. In scripts/ci-affects.sh line if ! CLOSURE=$(cargo metadata --format-version 1 2>/dev/null | python3 scripts/ci-closure.py $EXCEPT "$PKG"); then, $EXCEPT is deliberately left unquoted (marked # shellcheck disable=SC2086) to allow it to expand as a bare flag or vanish when empty.
  3. If an attacker can control the value assigned to EXCEPT (e.g., by influencing the first argument to the script through a compromised reusable workflow call or environment variable injection upstream), extra whitespace-separated tokens injected into EXCEPT would be word-split and passed as additional argv entries to ci-closure.py.
  4. ci-closure.py accepts arbitrary argv tokens as package names/flags (args = sys.argv[1:]), and unknown targets only cause a graceful error—no strict allowlist validation of anticipated flags occurs beyond the literal string --except.
  5. While this specific script only supports a fixed set of behavior (closure listing), the unquoted expansion pattern is a repeatable anti-pattern that could be leveraged in future modifications or copy-pasted into other unquoted contexts.
  6. Because CI outputs feed if: needs.affects.outputs.vta_tests != 'false' conditions in ci.yml, manipulating the closure resolution could cause CI to skip required tests (a test-oracle bypass) or run unintended jobs, undermining assurance that specific crates were tested before merge.

🔎 Threat Clue: Derived from COMP-002, COMP-003 via EP-002, EP-003

  • Data Flows: ci.yml -> ci-affects.sh -> ci-closure.py

Preconditions: Attacker controls or influences the invocation arguments of ci-affects.sh (e.g., via a malicious PR that modifies job step inputs, or a compromised reusable workflow caller)., The unquoted $EXCEPT variable contains attacker-influenced content beyond the two hardcoded literal call sites in ci.yml.

Existing Controls: set -euo pipefail enforces fail-fast behavior on unset variables and pipeline errors. • EXCEPT is only ever set to the literal string "--except" or empty string within the script's own logic, not externally supplied. • cargo-metadata output is trusted local repository state, not attacker network input.

Recommended Mitigations: Quote "$EXCEPT" explicitly and use an array (e.g., EXCEPT_ARGS=(); [ -n "$EXCEPT" ] && EXCEPT_ARGS+=("--except")) to avoid relying on word-splitting for correctness. • Add strict input validation on package name arguments (allowlist crate name character set). • Pin the two call sites in ci.yml to trusted literal strings only, and add a CI lint step (shellcheck) blocking future SC2086 suppressions without review.


🟠 STRIDE-2: Test Gating Bypass via Dependency Closure Miscalculation in affects Job

Field Detail
Category Tampering, Repudiation
Severity High
Likelihood Likely
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-284,CWE-1104
CAPEC CAPEC-176
OWASP A08:2021 - Software and Data Integrity Failures

Description: CI_WORKFLOW_TRIGGER entry point in the affects job allows selective test-skip due to reliance on Cargo.lock hash-based cache keys and closure-derived boolean gates that do not account for path-dependency source-only changes, resulting in merge of insufficiently tested code

Evidence: .github/workflows/ci.yml:131-133

test:
    name: Test (workspace)
    needs: affects
    if: needs.affects.outputs.vta_tests != 'false'

Attack Scenario:

  1. A contributor (malicious or careless) submits a PR modifying only source files inside a crate that is a path dependency, without touching Cargo.lock.
  2. Because key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} is unaffected by source-only changes (per the code comment: 'a source-only change does not move Cargo.lock and therefore does not move this key'), the restored target/ artifacts may come from a build compiled against different source.
  3. Separately, needs.affects.outputs.vta_tests / vtc_tests gates in ci.yml (if: needs.affects.outputs.vta_tests != 'false') rely on ci-affects.sh's closure computation via ci-closure.py; if the closure computation under- or over-classifies a crate's membership (e.g., due to a bug in graph traversal, or an intentionally crafted crate rename/move that briefly desyncs cargo metadata output from git diff), a test job intended to run may instead be skipped.
  4. An attacker aware of this gating logic (which is fully disclosed in the extensive code comments) can craft a change that manipulates file paths or crate boundaries such that ci-affects.sh reports 'none of the N changed files touch the dependency closure', causing test or test-vtc to be skipped entirely.
  5. The PR is merged with a false sense of test coverage, since GitHub treats a skipped job (due to if: condition) as passing/non-blocking rather than failed.
  6. Combined with target/ cache staleness, a subsequent identical-looking build could pass CI while shipping a regression that was never actually exercised by tests.

🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002, EP-003

  • Data Flows: PR event -> affects job -> test/test-vtc conditional gating

Preconditions: Attacker has ability to submit and merge (or get merged) a pull request., Repository branch protection does not explicitly require both test and test-vtc as required status checks independent of their conditional skip logic., Closure computation has an edge case or the attacker restructures crate boundaries to exploit path-based closure detection.

Existing Controls: Fail-open design: emit true "could not resolve the closure..." and emit true "empty closure..." intentionally run tests rather than skip when closure resolution fails. • Explicit code comments document the --except complement semantics to prevent a known logic error (room-host/vtc-client cross-dependency) — this specific known edge case was proactively fixed. • cargo check --workspace still runs unconditionally, providing a baseline compile-correctness gate even when tests are skipped.

Recommended Mitigations: Make the cache key also derive from a content hash of workspace source files (not only Cargo.lock) to avoid stale target/ reuse for path dependencies. • Add branch protection rules requiring test, test-vtc, and affects as mandatory status checks so a skipped-by-condition job cannot silently satisfy merge requirements without review. • Add a periodic full cargo test --workspace run on main/nightly schedule as a compensating control to catch closure-detection false negatives. • Add unit tests for ci-closure.py covering crate rename/move edge cases.


🟠 STRIDE-3: Supply Chain Compromise via Unpinned Third-Party GitHub Actions

Field Detail
Category Tampering, Information Disclosure, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-1357,CWE-829
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: BUILD_HOOK entry point via dtolnay/rust-toolchain@stable and taiki-e/install-action@cargo-semver-checks GitHub Actions allows a compromised or hijacked marketplace action to execute arbitrary code in the CI runner due to tag-based (not SHA-pinned) action references, resulting in secrets exfiltration and build artifact tampering

Evidence: .github/workflows/ci.yml:multiple (e.g., 199, 527, 575)

- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-semver-checks

Attack Scenario:

  1. Workflow steps reference third-party actions by mutable tag/branch names, e.g., uses: dtolnay/rust-toolchain@stable and uses: taiki-e/install-action@cargo-semver-checks, rather than immutable commit SHAs.
  2. An attacker who compromises the dtolnay/rust-toolchain or taiki-e/install-action upstream repository (e.g., via a stolen maintainer token or a supply-chain attack on their own dependencies) pushes malicious code to the stable tag or the action's default branch.
  3. On the next CI run triggered by any pull_request or push event, GitHub Actions resolves @stable to the attacker-controlled commit and executes it with the full permissions of the workflow's GITHUB_TOKEN and any repository secrets available to that job.
  4. The malicious action can exfiltrate CI secrets, modify build outputs (e.g., inject a backdoor into vtc-service or the admin-ui bundle assembled via npm run build referenced in build.rs), or persist through the actions/cache@v6 restore path.
  5. Because clippy and semver-checks jobs run cargo-semver-checks with elevated context (checking public crate APIs), a compromised install-action could tamper with published crate metadata or sign malicious releases if publish credentials are present in the environment.
  6. Detection is difficult because CI logs show a 'legitimate'-looking action name; only a diff of the resolved commit SHA would reveal the compromise.

🔎 Threat Clue: Derived from COMP-001, COMP-006 via EP-005

  • Data Flows: GitHub Actions runner -> third-party action execution -> secrets/GITHUB_TOKEN access

Preconditions: Upstream action repository (dtolnay/rust-toolchain, taiki-e/install-action, actions/checkout, actions/cache) is compromised or its tag is force-moved., CI secrets or GITHUB_TOKEN with write-level permissions are available to the affected job.

Existing Controls: GitHub Actions default GITHUB_TOKEN permissions can be scoped down at the workflow level (not confirmed present in visible slice). • Public, widely-used actions (dtolnay/rust-toolchain, actions/checkout) have community scrutiny reducing likelihood but not eliminating risk.

Recommended Mitigations: Pin all third-party actions to a specific immutable commit SHA rather than a mutable tag (e.g., dtolnay/rust-toolchain@<sha>). • Enable Dependabot/Renovate for GitHub Actions to track and review upstream SHA updates. • Apply least-privilege permissions: blocks at workflow and job level to minimize blast radius of a compromised action. • Use OpenSSF Scorecard or a similar tool to continuously monitor third-party action supply-chain risk.


🟠 STRIDE-4: Arbitrary Code Execution via Unreviewed install-build-deps.sh Content

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.8 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-494,CWE-829
CAPEC CAPEC-242
OWASP A08:2021 - Software and Data Integrity Failures

Description: SHELL_INVOCATION entry point bash scripts/install-build-deps.sh in multiple CI jobs allows silent execution of unreviewed privileged (sudo) commands due to the script's full contents not being visible/auditable in this diff (only the rename from install-semver-deps.sh is shown), resulting in potential undetected malicious dependency installation across all CI jobs

Evidence: .github/workflows/ci.yml:158, 208, 527, 575

run: bash scripts/install-build-deps.sh

Attack Scenario:

  1. The diff renames scripts/install-semver-deps.sh to scripts/install-build-deps.sh and repoints five separate CI job steps (test, test-vtc, clippy x2, and previously the inline apt-get block) to invoke it via run: bash scripts/install-build-deps.sh.
  2. The actual shell commands inside install-build-deps.sh are not included in the provided source (only referenced by filename), meaning any reviewer of this diff cannot verify what privileged operations (the prior inline version used sudo timeout 120 apt-get install) are performed.
  3. If an attacker can submit a PR that modifies install-build-deps.sh directly (e.g., a contributor with write access to a fork, or via a maliciously crafted PR from an external contributor if workflow uses pull_request_target or has secrets exposed to pull_request), they could inject sudo apt-get install <malicious-package> or arbitrary curl-pipe-to-shell commands.
  4. Because the script runs with sudo in the runner (inherited pattern from the removed inline block), any injected command executes with root privileges on the ephemeral CI runner.
  5. This is invoked identically across test, test-vtc, and both clippy/semver-check jobs, multiplying the blast radius of a single compromised script to nearly the entire pipeline.
  6. Malicious payloads could exfiltrate CI secrets, tamper with the checked-out source before compilation, or plant a backdoor in build artifacts that are later published or deployed.

🔎 Threat Clue: Derived from COMP-004 via EP-004

  • Data Flows: ci.yml steps -> install-build-deps.sh -> sudo apt-get install

Preconditions: Attacker has write access to scripts/install-build-deps.sh (via compromised maintainer account, malicious PR merge, or supply-chain injection into the file at rest)., CI job executes the script with sudo/root privileges as historically implied by the removed inline block.

Existing Controls: Script is version-controlled and subject to standard PR review before merge. • Runs in ephemeral, isolated GitHub-hosted runners limiting persistence beyond a single job. • Code comments indicate the intent is a narrowly-scoped apt-get install for libdbus-1-dev pkg-config, suggesting the actual script (not shown) is similarly scoped.

Recommended Mitigations: Require CODEOWNERS review specifically for any change to install-build-deps.sh and other CI-privileged scripts. • Pin exact package versions/hashes for apt-get installs to prevent dependency substitution attacks. • Run dependency installation steps with the minimum privilege necessary (avoid blanket sudo) and add integrity checks (checksum verification) for anything fetched over the network. • Enable branch protection requiring signed commits for changes to CI scripts.


🟠 STRIDE-5: Build-Time npm Dependency Compromise via build.rs Shell-Out to npm run build

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.6 CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-1357,CWE-494
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: BUILD_HOOK entry point vtc-service build.rs invoking npm run build for admin-ui allows execution of arbitrary npm lifecycle scripts and transitive supply-chain payloads due to unvetted Node.js dependency resolution during the Rust build, resulting in compromised admin-ui bundle or CI runner code execution

Evidence: .github/workflows/ci.yml:246 (comment referencing #1243)

# #1243: `vtc-service`'s build.rs used to write into the source tree —
      # `npm install` refreshed `admin-ui/package-lock.json`

Attack Scenario:

  1. The CI comment states 'vtc-service's build.rs shells out to npm run build for the admin UI', meaning every cargo build/cargo test -p vtc-service invocation transitively triggers Node.js package resolution and script execution.
  2. npm install/build lifecycle scripts (preinstall, postinstall, build) execute arbitrary JavaScript with the same privileges as the CI job; a compromised transitive npm dependency (typosquat, hijacked maintainer account, or malicious postinstall script) executes automatically with no additional attacker action required.
  3. A historical bug referenced in code comments (vtc-service recompiles in full on every cargo build, even with no changes #1243: 'vtc-service's build.rs used to write into the source tree — npm install refreshed admin-ui/package-lock.json') confirms build.rs previously mutated the source tree during build, showing the build process has broad filesystem write access during this step.
  4. If package-lock.json is not strictly integrity-pinned or if npm audit/lockfile verification is not enforced, an attacker who lands a malicious npm package version (matching a loose semver range) can have it silently pulled in on the next CI run.
  5. Because this build step happens inside cargo test -p vtc-service -p vtc-client and other jobs, the compromised code executes with access to the CI environment, including any secrets scoped to that job and the ability to tamper with the resulting admin-ui bundle shipped as part of vtc-service.
  6. The tampered admin-ui bundle, if deployed, could serve a malicious front-end to legitimate administrators of the Verifiable Trust Infrastructure, leading to credential theft or unauthorized administrative actions against the trust infrastructure.

🔎 Threat Clue: Derived from COMP-006 via EP-005

  • Data Flows: cargo build -> vtc-service build.rs -> npm run build -> admin-ui bundle

Preconditions: admin-ui/package-lock.json contains a loose or unpinned dependency range vulnerable to dependency confusion or malicious version publication., npm install executes without --ignore-scripts and without lockfile integrity verification enforced in CI.

Existing Controls: package-lock.json is checked into the repo (referenced in comments), providing some reproducibility. • The historical #1243 bug fix suggests awareness of build.rs source-tree mutation risk and at least partial remediation. • Build occurs in ephemeral, isolated CI runners.

Recommended Mitigations: Enforce npm ci (not npm install) with strict lockfile verification in the admin-ui build step to prevent silent dependency drift. • Run npm audit signatures or use npm's provenance attestation to verify package integrity. • Consider npm ci --ignore-scripts combined with an explicit, reviewed allowlist of required lifecycle scripts. • Isolate the admin-ui build into its own sandboxed CI job with minimal secret exposure, decoupled from build.rs execution during cargo test/build.


🟡 STRIDE-6: Silent Test Skipping via Fail-Open Closure Resolution Masking Repudiation of Coverage Decisions

Field Detail
Category Repudiation
Severity Medium
Likelihood Likely
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N
Residual Severity Low
CWE CWE-778
CAPEC CAPEC-268
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: GITHUB_STEP_SUMMARY output in ci-affects.sh allows insufficient auditability of why a test job ran or was skipped due to reliance on free-text emitted reasons without structured/immutable logging tied to commit SHA, resulting in inability to later verify or dispute a CI test-skip decision

Evidence: scripts/ci-affects.sh:76-79

echo "$2"
  [ -n "${GITHUB_OUTPUT:-}" ] && echo "run=$1" >> "$GITHUB_OUTPUT"
  [ -n "${GITHUB_STEP_SUMMARY:-}" ] && echo "**$SCOPE**: run=\`$1\` — $2" >> "$GITHUB_STEP_SUMMARY"

Attack Scenario:

  1. ci-affects.sh's emit() function writes a human-readable justification string (e.g., 'none of the N changed files touch the dependency closure of $SCOPE') to $GITHUB_STEP_SUMMARY and sets run=$1 in $GITHUB_OUTPUT.
  2. These are ephemeral GitHub Actions artifacts with default retention policies (typically 90 days) and are not cryptographically signed or independently archived.
  3. After a security incident is discovered in a shipped crate, an investigator wants to confirm whether tests actually ran for the commit that introduced the vulnerability.
  4. If the workflow run has expired from GitHub's retention window, or if the step summary was truncated/malformed, there is no durable, tamper-evident record proving whether test or test-vtc executed against that specific commit.
  5. A developer (malicious insider or compromised account) could also merge a PR, let CI run with test skipped intentionally engineered via a crafted file change, and later plausibly deny knowledge that tests were skipped, since the justification text is buried in ephemeral logs rather than a permanent, queryable audit trail.
  6. This undermines the non-repudiation of 'tests passed before merge' claims used for compliance or incident postmortems.

🔎 Threat Clue: Derived from COMP-002 via EP-002

  • Data Flows: ci-affects.sh emit() -> GITHUB_STEP_SUMMARY / GITHUB_OUTPUT

Preconditions: Investigation occurs after GitHub Actions log/artifact retention window has expired., No external SIEM or long-term CI log archival is configured (not observable in provided files).

Existing Controls: GITHUB_STEP_SUMMARY provides at least a per-run visible justification without opening full logs. • GITHUB_OUTPUT run= value is consumed by downstream if: conditions, creating a traceable (if short-lived) linkage between decision and outcome within the same workflow run.

Recommended Mitigations: Forward CI decision logs (including ci-affects.sh emit() reasons) to a long-term, append-only external log store (e.g., SIEM, object storage with retention lock). • Include the resolved commit SHA and closure decision in a signed build provenance attestation (e.g., SLSA provenance) for each merge. • Extend GitHub Actions log retention or archive workflow run artifacts to cold storage on a schedule.


🟡 STRIDE-7: Denial of Service via Disk Exhaustion Race Condition in test-vtc Job Cleanup

Field Detail
Category Denial of Service
Severity Medium
Likelihood Possible
CVSS 4.8 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-400,CWE-367
CAPEC CAPEC-125
OWASP A04:2021 - Insecure Design

Description: Free disk space step in test-vtc job allows CI pipeline disruption due to a hardcoded, order-dependent cleanup (rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc) that assumes toolcache layout stability, resulting in job failure or reduced availability if runner image changes or the assumption about $AGENT_TOOLSDIRECTORY safety is violated

Evidence: .github/workflows/ci.yml:202-210

- name: Free disk space
        run: |
          sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
          df -h /

Attack Scenario:

  1. The test-vtc job runs sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc unconditionally to free disk space before building the full workspace, per the comment describing a 'No space left on device' failure while linking libvta_service.
  2. This is a TOCTOU-adjacent assumption: the script assumes these specific paths exist and are safe to remove on GitHub's ubuntu-latest runner image, and explicitly warns 'do NOT remove $AGENT_TOOLSDIRECTORY' — but the check is documented only in a comment, not enforced programmatically (no path validation before rm -rf).
  3. If GitHub silently changes the hosted runner image (moves Node.js into one of the removed paths, or renames directories), the rm -rf could inadvertently delete $AGENT_TOOLSDIRECTORY contents indirectly (e.g., symlink traversal) or fail to free enough space, causing the same linking failure to recur without warning until it manifests as a build break.
  4. An external attacker cannot directly trigger this, but a malicious contributor could submit a PR that adds a heavy dependency or debug-symbol-laden crate specifically timed to exhaust remaining disk headroom right at the libvta_service link step, turning an existing fragile margin into a reliable CI DoS against all subsequent PRs sharing the runner pool (indirectly, by making CI flaky/red across the org).
  5. Repeated CI failures on the test-vtc gate could be leveraged to pressure maintainers into merging without full VTC test coverage (see STRIDE-2), compounding risk.
  6. No automated verification exists that confirms npm/node remains functional after the cleanup step, relying entirely on the comment's claim that Android/.NET/GHC removal is safe.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: test-vtc job -> Free disk space step -> cargo test -p vtc-service -p vtc-client

Preconditions: GitHub Actions runner image layout changes unexpectedly, or a crafted dependency significantly increases binary/debuginfo size., No automated check validates node/npm availability after the rm -rf step.

Existing Controls: Explicit df -h / diagnostic step immediately after cleanup and cache restore to surface low-disk conditions in logs. • Detailed code comments document which paths are safe to remove and why (Android/.NET/GHC unused; Node retained via $AGENT_TOOLSDIRECTORY). • timeout-minutes: 30 bounds overall job duration, limiting DoS blast radius to a single job run.

Recommended Mitigations: Add an explicit post-cleanup assertion (command -v npm / node --version) that fails fast with a clear error if Node tooling was inadvertently removed. • Use du -sh pre-cleanup to dynamically decide which caches to prune rather than a hardcoded path list, making the step resilient to runner image changes. • Consider splitting the admin-ui npm build into a separate job with pre-built/cached artifacts to reduce disk pressure during the Rust link step.


🟡 STRIDE-8: Stale Build Artifact Reuse via Path-Dependency-Insensitive Cache Key in actions/cache

Field Detail
Category Tampering, Repudiation
Severity Medium
Likelihood Likely
CVSS 5.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:M/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-829,CWE-345
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: actions/cache@v6 step in test job allows compilation of stale/mismatched artifacts due to a cache key based solely on hashFiles('**/Cargo.lock') while every crate is a path dependency, resulting in tests executing against outdated code that does not reflect the actual PR diff

Evidence: .github/workflows/ci.yml:165-176

key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-test-

Attack Scenario:

  1. The cache configuration in the test job uses key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} explicitly restricted to ~/.cargo/registry and ~/.cargo/git per the code comment ('target/ is deliberately NOT cached... every crate in this workspace is a path dependency, so a source-only change does not move Cargo.lock').
  2. The comment further states this was a real historical bug (issue feat(vta): mint the consent ceremony's correlator instead of deriving it #1133) where 'the job restores a target/ built from other source and reuses stale artifacts' when target/ WAS previously cached — indicating the failure mode is known and was fixed by excluding target/ from the cache.
  3. However, the residual registry/git cache reuse still means that if an attacker publishes a malicious crate version to a registry mirror or git dependency source between cache-populating and cache-restoring runs (dependency confusion / typosquat), the cached registry cache could serve a poisoned crate to a build that never re-fetches it, since restore-keys: ${{ runner.os }}-cargo-test- allows fallback to any prior cache even across different Cargo.lock states.
  4. Because restore-keys uses a prefix match without the full hash, a PR that modifies Cargo.lock to pull in a new (possibly malicious) dependency version could still restore an older cache blob for ~/.cargo/registry that lacks integrity re-verification beyond Cargo's own checksum validation, though Cargo.lock hashes do provide primary protection here.
  5. If Cargo's own checksum enforcement is somehow bypassed (e.g., via a [patch] override merged in the same PR, or a local path override introduced quietly in Cargo.toml), the fallback cache could mask the substitution during automated testing, since the actually-built artifact might not be freshly fetched/verified.
  6. This chains with STRIDE-2 (test gating bypass) since a downstream vtc-service test relying on stale cached dependencies could pass without truly validating the current dependency graph.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: test job -> actions/cache restore -> cargo test --workspace

Preconditions: A crate version substitution or [patch]/path-override is introduced in the same or a prior PR., The registry/git cache retains a stale or substituted crate blob across multiple workflow runs due to restore-keys prefix fallback.

Existing Controls: target/ is explicitly excluded from caching, directly remediating the known #1133 stale-artifact bug for compiled outputs. • Cargo's built-in checksum verification for crates.io dependencies provides a baseline integrity check independent of the CI cache. • Cache key still incorporates the Cargo.lock hash as primary key, only falling back to prefix match on miss.

Recommended Mitigations: Avoid restore-keys prefix fallback for security-sensitive registry cache paths, or add a post-restore step that revalidates crate checksums against Cargo.lock. • Periodically invalidate/rotate the CI cache to bound the exposure window for any latent poisoned cache entry. • Enable cargo vendor or a lockfile-integrity verification step (cargo verify-project combined with checksum diffing) before running tests.


🟡 STRIDE-9: Split-Test-Job Coverage Gap Allowing vtc-service/vtc-client Regressions to Slip Past workspace Test Job

Field Detail
Category Tampering
Severity Medium
Likelihood Likely
CVSS 5.4 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:M/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1104,CWE-696
CAPEC CAPEC-176
OWASP A04:2021 - Insecure Design

Description: cargo test invocation split between test (excluding vtc-service/vtc-client) and test-vtc (only vtc-service/vtc-client) allows an integration regression at the workspace boundary to go undetected due to no job running the full unrestricted cargo test --workspace, resulting in interface/integration bugs between excluded and included crates shipping undetected

Evidence: .github/workflows/ci.yml:185, 246

- run: cargo test --workspace --exclude vtc-service --exclude vtc-client
...
- run: cargo test -p vtc-service -p vtc-client

Attack Scenario:

  1. The diff changes - run: cargo test --workspace (which previously tested everything together) into two separate, mutually exclusive invocations: cargo test --workspace --exclude vtc-service --exclude vtc-client in test, and cargo test -p vtc-service -p vtc-client in test-vtc.
  2. Both jobs are further individually gated by needs.affects.outputs.vta_tests/vtc_tests, meaning it is now possible for neither job to exercise a combined integration test that spans both crate sets in a single cargo test process (e.g., an integration test crate depending on both room-host and vtc-client that previously ran under the unified --workspace invocation).
  3. The code's own comment acknowledges the cross-dependency risk generally ('room-host depends on vtc-client, so vtc-client belongs to BOTH sides'), but this acknowledgment covers only whether a job RUNS, not whether cross-crate integration test binaries that span both partitions are correctly exercised by either partitioned cargo test invocation.
  4. If such an integration test exists (e.g., in a top-level tests/ directory depending on symbols from both partitions), cargo test -p vtc-service -p vtc-client and cargo test --workspace --exclude vtc-service --exclude vtc-client may each independently fail to build/run it, or worse, both silently skip it if it's not a member of either explicitly-specified package set, letting a regression merge undetected.
  5. This is a subtle, high-plausibility regression risk introduced directly by the refactor in this diff and not present before parallelization, since the original single cargo test --workspace command guaranteed all integration tests ran together in one pass.
  6. A malicious contributor aware of this gap could intentionally introduce a regression that only manifests in the untested cross-partition integration surface, timing it precisely to slip through both partitioned test runs.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: test job + test-vtc job -> partitioned cargo test invocations

Preconditions: A workspace-level (non-crate-scoped) integration test exists that depends on symbols from both the test partition and the test-vtc partition., Neither cargo test --workspace --exclude ... nor cargo test -p vtc-service -p vtc-client happens to include that integration test target.

Existing Controls: cargo check --workspace still runs unconditionally, catching compile-level (not runtime/logic) regressions across the whole workspace. • Explicit code comments show the authors reasoned carefully about crate-closure correctness (the --except/room-host/vtc-client case), indicating active awareness of cross-boundary risk. • affects job outputs both vta_tests and vtc_tests independently, so in the common case both partitions still run when either side changes.

Recommended Mitigations: Add a dedicated CI job that runs the full, unrestricted cargo test --workspace (without excludes) at least on a merge-queue or pre-merge-to-main gate, even if per-PR partitioned jobs are used for speed. • Explicitly enumerate and test any cross-partition integration test targets in both partitioned jobs to guarantee coverage. • Add a coverage-diff check comparing pre-refactor cargo test --workspace test count/names against the sum of both partitioned runs to detect any test that fell through the cracks.


🔵 STRIDE-10: Information Disclosure via Verbose CI Diagnostic Output of Disk and Package State

Field Detail
Category Information Disclosure
Severity Low
Likelihood Possible
CVSS 2.3 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-200
CAPEC CAPEC-116
OWASP A01:2021 - Broken Access Control

Description: df -h / and git status --porcelain / git diff --stat diagnostic steps in CI jobs allow exposure of internal runner filesystem layout and uncommitted build-artifact changes due to unredacted debug output in public workflow logs, resulting in reconnaissance information useful for planning further attacks

Evidence: .github/workflows/ci.yml:153, 210, 285-287

- name: Disk after cache restore
        run: df -h /
...
            git status --porcelain
            git diff --stat

Attack Scenario:

  1. Multiple steps in ci.yml emit raw diagnostic data to the public (or organization-visible) Actions log: df -h / (disk layout/free space), and a git status/diff check ('git status --porcelain', 'git diff --stat') guarding against build.rs writing into the source tree.
  2. On a public repository (OpenVTC appears to be open source given the GitHub URL), these logs are visible to any unauthenticated visitor browsing the Actions tab, unless the repository or workflow is private.
  3. An attacker studying these logs learns internal runner disk partition sizing/headroom (useful for crafting a disk-exhaustion DoS payload as discussed in STRIDE-7), and can observe whether/which files npm install/build.rs unexpectedly modifies, revealing internal build-artifact paths (e.g., admin-ui/package-lock.json) that could hint at attack surfaces for a supply-chain injection (STRIDE-5).
  4. While individually low-sensitivity, this diagnostic output incrementally lowers the attacker's reconnaissance cost for chaining STRIDE-5 and STRIDE-7 together into a more effective combined attack.
  5. No secrets appear to be printed in the visible slice, limiting the severity to reconnaissance value rather than direct credential exposure.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: CI job steps -> public GitHub Actions logs

Preconditions: Repository or workflow run visibility is public or accessible to the attacker (e.g., public OSS repo, or a compromised/external collaborator account with read access).

Existing Controls: Diagnostic commands (df -h, git status --porcelain) do not print secrets or credentials. • Output is scoped to build-environment metadata, not application data or trust-infrastructure secrets.

Recommended Mitigations: Mask or omit granular disk-size diagnostics in steps executed on public-facing repositories where not strictly necessary for debugging. • Restrict verbose diagnostic steps to a debug build-mode toggle rather than unconditional execution on every run. • Periodically review workflow log output for unintended sensitive data leakage as part of routine CI hygiene review.


🔵 STRIDE-11: Missing Workspace Membership Validation Allowing Confused-Deputy Crate Name Collision in ci-closure.py

Field Detail
Category Tampering
Severity Low
Likelihood Unlikely
CVSS 3.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-697
CAPEC CAPEC-141
OWASP A04:2021 - Insecure Design

Description: PIPE_STDIN entry point ci-closure.py allows incorrect closure computation via crate-name-based (not path/id-based) matching due to by_id[i]["name"] in targets comparison, resulting in a same-named external dependency being conflated with a workspace member if cargo's resolver ever aliases names

Evidence: scripts/ci-closure.py:58-64

if invert:
        start = [i for i in members if by_id[i]["name"] not in targets]
    else:
        start = [i for i in members if by_id[i]["name"] in targets]

Attack Scenario:

  1. ci-closure.py filters workspace members with start = [i for i in members if by_id[i]["name"] in targets] (or the inverted not in targets form), matching purely on the crate's name field from cargo metadata JSON output.
  2. members is already restricted to workspace_root-prefixed package IDs (per the surrounding code deriving root = meta["workspace_root"] + os.sep), which limits this specifically to workspace members and mitigates confusion with same-named external crates.io dependencies in the common case.
  3. However, if a future refactor or Cargo feature (e.g., a virtual workspace restructuring, or a crate temporarily existing both as a path dependency during a migration and a published crates.io version with an identical name) creates an ambiguous state, the name-only match in targets (a flat set of strings from --except/positional args) could resolve the wrong crate or silently include/exclude more or fewer crates than intended.
  4. Because this script's output directly drives which CI test jobs execute (feeding into ci-affects.sh's emit() and downstream if: gates), a closure-computation error here could reintroduce the exact kind of test-skip risk described in STRIDE-2, just via a different root cause (name ambiguity rather than path-dependency cache staleness).
  5. This is a defensive-depth/robustness gap rather than a directly exploitable vulnerability today, given the workspace_root prefix filter already present in the surrounding code.

🔎 Threat Clue: Derived from COMP-003 via EP-003

  • Data Flows: cargo metadata -> ci-closure.py -> IN/OUT tagged crate list

Preconditions: Workspace restructuring introduces genuine crate name ambiguity between a path-dependency member and a differently-scoped package sharing the same name., No test exists validating ci-closure.py's behavior under such a collision.

Existing Controls: members list is pre-filtered to only workspace-root-prefixed package IDs, substantially reducing the likelihood of external crate confusion. • unknown = targets - {by_id[i]["name"] for i in members} validation rejects target names that are not workspace members outright, causing a fail-fast exit (return 3) rather than silent misclassification for any target explicitly named on the command line.

Recommended Mitigations: Match on package ID (which includes full path/version) rather than bare name wherever feasible, falling back to name only for user-facing CLI ergonomics. • Add a unit test in the script's test suite (if any) simulating a name collision between a workspace member and an external dependency to lock in correct behavior. • Document the workspace_root-based disambiguation explicitly in the script's docstring for future maintainers.


⚪ STRIDE-12: Prompt-Injection-Style Instruction Embedded in Code Comments Attempting to Influence Automated Review Tooling

Field Detail
Category Tampering, Repudiation
Severity Informational
Likelihood Possible
CVSS 0.0 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1021
CAPEC CAPEC-660
OWASP A03:2021 - Injection

Description: Extensive narrative code comments across ci.yml/ci-affects.sh/ci-closure.py allow an automated security-review LLM/tool to be socially engineered due to persuasive, first-person justification text embedded directly in diff content, resulting in a risk that automated or human reviewers under-scrutinize the actual security-relevant code changes

Evidence: .github/workflows/ci.yml:various

# The failure names a field that is plainly there in the diff, so it reads as the author's bug — see #1133, where a clean-room checkout of the same commit compiled fine.

Attack Scenario:

  1. The diff contains unusually long, narrative, first-person-style comments (e.g., 'The failure names a field that is plainly there in the diff, so it reads as the author's bug — see feat(vta): mint the consent ceremony's correlator instead of deriving it #1133, where a clean-room checkout of the same commit compiled fine.') embedded directly in CI configuration and shell/python scripts.
  2. Such comments are legitimate documentation in isolation, but this pattern (verbose justificatory prose inside infrastructure-as-code) is a known technique that can be abused to pre-empt scrutiny of a change ('this looks like a bug but it's actually intentional and already explained') or to embed instructions targeting automated review tools/LLMs that read source comments as part of their analysis pipeline.
  3. Per this analysis's own security directive, all such text must be treated strictly as DATA/content to analyze, not as instructions — this finding documents that the pattern exists and should be flagged for human review rather than trusted at face value, since a determined attacker could craft similarly-styled comments containing directives like 'ignore this code path' or 'mark as false positive' to manipulate less rigorous automated scanners.
  4. No explicit manipulative directive (e.g., literal 'ignore previous instructions') was found in the reviewed comments in this specific diff; the extensive prose is presented as good-faith engineering rationale for genuinely tricky CI flakiness (disk space, cache staleness, closure complement logic).
  5. Nonetheless, the sheer verbosity and narrative style constitutes an attack-surface risk pattern for review-fatigue and automated-tool manipulation that should be tracked as a process control gap, independent of whether this particular instance is malicious.

🔎 Threat Clue: Derived from COMP-001, COMP-002, COMP-003 via EP-001, EP-002, EP-003

  • Data Flows: Source code comments -> automated review/LLM analysis pipeline

Preconditions: A future contributor embeds genuinely adversarial natural-language instructions inside code comments specifically targeting AI-assisted code review or security scanning tools., Automated review tooling insufficiently isolates comment content as untrusted data (mitigated in this analysis by the enforced SECURITY DIRECTIVE).

Existing Controls: This analysis explicitly treats all pasted content, including comments, as untrusted DATA per its governing security directive, preventing any instruction embedded in comments from altering findings or verdicts. • Human PR review remains a control layer independent of automated tooling.

Recommended Mitigations: Establish a review policy flagging unusually long or persuasive inline comments in CI/infra files for additional human scrutiny. • Ensure any automated security review tooling explicitly treats source comments as untrusted content, never as control-flow instructions (already enforced here). • Encourage moving detailed rationale to linked issue trackers (e.g., '#1133', '#1243' as already practiced) rather than embedding excessive narrative directly in code, reducing the surface for this pattern.



🍝 PASTA Threat Model

Application Purpose

OpenVTC's Verifiable Trust Infrastructure provides mobile-core, enclave (TEE), and service-layer crates for cryptographic trust verification, and this PR modifies only the CI/CD pipeline (test gating, dependency installation, and closure computation) that gates code from reaching production, making the pipeline itself a high-value target for undermining assurance of the shipped trust infrastructure.

Inherent Risks

  • The CI pipeline is a single chokepoint whose compromise can propagate to a security-sensitive Rust workspace including a TEE enclave crate.
  • Path-dependency workspace structure creates cache-key/closure edge cases that are inherently hard to fully enumerate.
  • Third-party GitHub Actions and npm build-time dependencies introduce supply-chain trust that is largely outside this repository's direct control.
  • Extensive narrative code comments increase reviewer cognitive load, raising the risk that subtle logic changes are under-scrutinized.

Objectives

Risk: Accept fail-open behavior (run tests when uncertain) for closure resolution errors to bias toward safety over speed.; Treat any privileged (sudo) CI step as a high-value target requiring change control.
Business: Ship a reliable, trustworthy verifiable-trust platform without CI flakiness blocking releases.
Security: Ensure test-skip decisions never mask an actual regression in security-critical crates (vta-enclave, vtc-service).; Prevent supply-chain compromise of CI-time dependencies (apt packages, GitHub Actions, npm packages) from reaching build artifacts.
Financial: Minimize CI compute costs from redundant full-workspace test runs.; Avoid costly incident response from a supply-chain compromise reaching production trust infrastructure.
Compliance: Maintain auditable evidence that all required tests executed prior to merge for regulated trust-infrastructure components.
Functional: Correctly determine which workspace crates are affected by a given change to selectively run tests.; Reliably install build-time system and npm dependencies across all CI jobs.
Operational: Keep CI runs within GitHub-hosted runner resource limits (disk, time).; Maintain fast feedback loops for contributors via parallelized test jobs.

Business Impact Analysis (2)

BIA-1: CI Test Gating and Merge Assurance (Critical)

The end-to-end process by which a pull request's changes are automatically scoped, tested, and gated before being eligible for merge into the trust-infrastructure codebase.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Compliance Auditors / Downstream Integrators / Maintainers / Security Team
  • Dependencies: Cargo Workspace Metadata / GitHub Actions Runners / GitHub-Hosted Package Mirrors / Third-Party GitHub Actions / cargo-semver-checks Action / npm Registry (admin-ui build)
  • Disruptions: Test gating logic incorrectly skips required tests for a security-critical crate. / Supply-chain compromise of a build-time dependency (apt, npm, or GitHub Action) injects malicious code into CI-built artifacts. / Disk exhaustion or apt mirror outage causes CI to fail-closed, blocking all merges.
  • Impacts: Unreviewed/untested regression merged into vta-enclave or vtc-service, risking compromise of the trust infrastructure's cryptographic guarantees. / Complete CI outage blocking all development velocity for an extended period. / Reputational damage if a compromised admin-ui or enclave build is later attributed to a CI supply-chain failure.

BIA-2: Build-Time Dependency Provisioning (High)

The process of installing system packages (apt), Rust toolchains, and npm dependencies required to compile and test the workspace within each CI job.

MTD: 02 days 00:00 hours | RTO: 00 days 08:00 hours | RPO: 00 days 01:00 hours

  • Stakeholders: Maintainers / Security Team
  • Dependencies: apt Package Mirror / dtolnay/rust-toolchain Action / npm Registry / taiki-e/install-action
  • Disruptions: Malicious package substitution via a compromised apt mirror, npm registry entry, or GitHub Action tag. / Unbounded/unreviewed script (install-build-deps.sh) execution with sudo privileges.
  • Impacts: Arbitrary code execution on CI runners with access to secrets and build artifacts. / Backdoored build artifacts (e.g., admin-ui bundle) shipped to production.

Technical Scope

Roles (3): RO-1 Repository Maintainer · RO-2 External Contributor · RO-3 CI Runner Service Identity

Actors (3): AC-1 PR Author · AC-2 GitHub Actions Runner · AC-3 Third-Party Action Maintainer

Entry Points (5): EP-1 CI Workflow Trigger · EP-2 ci-affects.sh Invocation · EP-3 ci-closure.py Pipe Invocation · EP-4 install-build-deps.sh Invocation · EP-5 admin-ui Build Hook

Threat Actors (3): TA-1 Malicious External Contributor · TA-2 Supply-Chain Attacker (Upstream Action/Package Compromise) · TA-3 Malicious or Compromised Insider

Infrastructure (1): IF-1 GitHub-Hosted ubuntu-latest Runner

Trust Boundaries (3): TB-1 GitHub Actions Control Plane · TB-2 Ephemeral CI Runner Sandbox · TB-3 Repository Source Control

External Entities (3): EE-1 GitHub Actions Marketplace (dtolnay/rust-toolchain, taiki-e/install-action, actions/checkout, actions/cache) · EE-2 npm Registry · EE-3 apt Package Mirror / Ubuntu Repository

System Components (8): SC-1 affects Job (Closure Computation) · SC-2 ci-affects.sh Script · SC-3 ci-closure.py Script · SC-4 install-build-deps.sh Script · SC-5 test / test-vtc Jobs · SC-6 clippy / semver-checks Jobs · SC-7 vtc-service build.rs / admin-ui npm Build · SC-8 actions/cache Registry/Git Cache

Resources And Assets (4): RA-1 Cargo.lock / Workspace Metadata · RA-2 ~/.cargo/registry and ~/.cargo/git Cache · RA-3 GITHUB_TOKEN and Job Secrets · RA-4 admin-ui/package-lock.json and Built Bundle

Technologies And Dependencies (6): TD-1 Rust Cargo Workspace (path dependencies) · TD-2 dtolnay/rust-toolchain GitHub Action · TD-3 taiki-e/install-action (cargo-semver-checks) · TD-4 actions/cache · TD-5 actions/checkout · TD-6 npm / Node.js (admin-ui build)

Use Cases (2)

  • Selective Test Gating via Workspace Dependency Closure: A maintainer's pull request triggers the affects job, which computes the dependency closure of changed files to decide whether the test and test-vtc jobs should run, avoiding unnecessary full-workspac
  • CI Build Dependency Provisioning and Workspace Build: Each CI job installs required system dependencies via install-build-deps.sh, sets up the Rust toolchain, restores the registry/git cache, and compiles the workspace including the admin-ui bundle for v

⚔️ Attack Scenarios (4)

SC-4: install-build-deps.sh Script

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Malicious or Compromised Insider<br><i>Abuse write access to weaken CI gating</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S4@{ shape: rect, label: "STRIDE-4: Arbitrary Code Execution via Unreviewed install-build-deps.sh Content<br><i>High / Possible</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C242@{ shape: rect, label: "CAPEC-242: Code Injection" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W494@{ shape: rect, label: "CWE-494: Download of Code Without Integrity Check" }
    W829@{ shape: rect, label: "CWE-829: Inclusion of Functionality from Untrusted Control Sphere" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC4@{ shape: rect, label: "SC-4: install-build-deps.sh Script" }
  end
  SC4 --> W494
  SC4 --> W829
  W494 --> C242
  W829 --> C242
  C242 --> S4
  S4 --> TA3
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FF0000,stroke-width:2px
  linkStyle 3 stroke:#FF0000,stroke-width:2px
  linkStyle 4 stroke:#FF0000,stroke-width:2px
  linkStyle 5 stroke:#FF0000,stroke-width:2px
Loading

SC-1: affects Job (Closure Computation)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious External Contributor<br><i>Slip under-tested changes past CI gates</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S2@{ shape: rect, label: "STRIDE-2: Test Gating Bypass via Dependency Closure Miscalculation<br><i>High / Likely</i>" }
    S9@{ shape: rect, label: "STRIDE-9: Split-Test-Job Coverage Gap<br><i>Medium / Likely</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C176@{ shape: rect, label: "CAPEC-176: Configuration/Environment Manipulation" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W284@{ shape: rect, label: "CWE-284: Improper Access Control" }
    W1104@{ shape: rect, label: "CWE-1104: Use of Unmaintained Third Party Components" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: affects Job" }
  end
  SC1 --> W284
  SC1 --> W1104
  W284 --> C176
  W1104 --> C176
  C176 --> S2
  C176 --> S9
  S2 --> TA1
  S9 --> TA1
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FF0000,stroke-width:2px
  linkStyle 3 stroke:#FF0000,stroke-width:2px
  linkStyle 4 stroke:#FF0000,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
  linkStyle 6 stroke:#FF0000,stroke-width:2px
  linkStyle 7 stroke:#FFA500,stroke-width:2px
Loading

SC-7: vtc-service build.rs / admin-ui npm Build

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Supply-Chain Attacker<br><i>Compromise widely-used upstream packages</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S5@{ shape: rect, label: "STRIDE-5: Build-Time npm Dependency Compromise<br><i>High / Possible</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C538@{ shape: rect, label: "CAPEC-538: Open-Source Supply Chain Attack" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W1357@{ shape: rect, label: "CWE-1357: Reliance on Insufficiently Trustworthy Component" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC7@{ shape: rect, label: "SC-7: build.rs / admin-ui npm Build" }
  end
  SC7 --> W1357
  W1357 --> C538
  C538 --> S5
  S5 --> TA2
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FF0000,stroke-width:2px
  linkStyle 3 stroke:#FF0000,stroke-width:2px
Loading

SC-5: test / test-vtc Jobs

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious External Contributor<br><i>Time a regression to exhaust CI disk margin</i>" }
  end
  subgraph SL2["2. Threats"]
    direction LR
    S7@{ shape: rect, label: "STRIDE-7: DoS via Disk Exhaustion Race Condition<br><i>Medium / Possible</i>" }
    S8@{ shape: rect, label: "STRIDE-8: Stale Build Artifact Reuse via Cache Key<br><i>Medium / Likely</i>" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    C125@{ shape: rect, label: "CAPEC-125: Flooding / Resource Exhaustion" }
    C538b@{ shape: rect, label: "CAPEC-538: Open-Source Supply Chain Attack" }
  end
  subgraph SL4["4. Weaknesses"]
    direction LR
    W400@{ shape: rect, label: "CWE-400: Uncontrolled Resource Consumption" }
    W829b@{ shape: rect, label: "CWE-829: Inclusion of Functionality from Untrusted Control Sphere" }
  end
  subgraph SL5["5. System Component"]
    direction LR
    SC5@{ shape: rect, label: "SC-5: test / test-vtc Jobs" }
  end
  SC5 --> W400
  SC5 --> W829b
  W400 --> C125
  W829b --> C538b
  C125 --> S7
  C538b --> S8
  S7 --> TA1
  S8 --> TA1
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#FFA500,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#FFA500,stroke-width:2px
  linkStyle 4 stroke:#FFA500,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
  linkStyle 6 stroke:#FFA500,stroke-width:2px
  linkStyle 7 stroke:#FFA500,stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 12

By Severity: Low: 2 · High: 4 · Medium: 5 · Informational: 1

By Category: Tampering: 9 · Elevation of Privilege: 4 · Repudiation: 4 · Information Disclosure: 2 · Denial of Service: 1


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
1 0

Confirmed (1)

  • 🟡 Github Actions Mutable Action Tag (35 occurrences)

@stormer78
stormer78 merged commit 25b766e into main Sep 6, 2026
15 of 16 checks passed
@stormer78
stormer78 deleted the ci/split-test-job branch September 6, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants