Intentionally vulnerable CI/CD workflow-security testbed. Not for production use.
This public repository is the isolated fixture corpus for validating REACHABLE CI/CD workflow-code analysis. It focuses on GitHub Actions, Cordyceps-style workflow attacks, chained pipeline trust boundaries, reusable-workflow authority edges, and defended control examples.
The workflows are designed for scanner validation and repeatable release proof. They are not application workflows and must not be copied into production repositories.
The machine-readable baseline is expected/workflow-security.json. The public proof target is:
| Dimension | Expected |
|---|---|
| Workflow fixture files | 40 |
| Native-positive workflow files | 31 |
| Zero-native fixture files | 10 |
| Native REACHABLE workflow-security findings | 146 |
| Critical native findings | 30 |
| High-risk native findings | 100 |
| Medium-risk native findings | 10 |
| Low-risk native findings | 6 |
| Required risk classes | 23 |
| Defended native-control files | 4 |
Optional tools such as zizmor and actionlint may add corroborating rows.
The stable validation contract is the exact native REACHABLE finding set, the
required class detections, and the zero-native fixture paths documented in
expected/workflow-security.json.
The native baseline intentionally excludes duplicate generic secret-scanner rows
for workflow YAML; workflow secret references are validated as workflow
authority evidence. Path-backed findings are validated from persisted workflow
review payload when the DB raw-data copy is size-capped.
- Every intentionally vulnerable GitHub job is guarded with
if: ${{ false }}and GitLab jobs use inert rules such aswhen: never. - Publishing, deployment, and secret-use examples are inert simulations. They
use
echoor disabled jobs, not real package, release, or cloud operations. - No real secrets are stored in this repository.
- The expected findings are documented in EXPECTED.md and expected/workflow-security.json.
The support corpus proves that REACHABLE treats workflow code as a first-class security surface, separate from application source code and package inventory.
| Area | Fixture | Expected class |
|---|---|---|
| Cordyceps command injection | .github/workflows/cordyceps-command-injection.yml |
cicd_command_injection |
| Cordyceps code injection | .github/workflows/cordyceps-code-injection.yml |
cicd_code_injection |
| Cordyceps broken authorization | .github/workflows/cordyceps-broken-authorization.yml |
cicd_auth_logic_error |
| Cordyceps artifact poisoning | .github/workflows/cordyceps-artifact-producer.yml and .github/workflows/cordyceps-artifact-consumer.yml |
cicd_artifact_integrity_gap, cicd_cross_workflow_poisoning |
| Secret authority exposure | .github/workflows/secret-authority-exposure.yml |
cicd_secret_authority_exposure |
| Self-hosted runner exposure | .github/workflows/self-hosted-runner-exposure.yml |
cicd_self_hosted_runner_exposure |
| Mutable action refs | vulnerable workflow action references | cicd_unpinned_action |
| Reusable boundary / inherited secrets | .github/workflows/reusable-untrusted-caller.yml calling .github/workflows/reusable-inherit-secrets.yml with secrets: inherit |
cicd_cross_workflow_poisoning, cicd_secret_authority_exposure, cicd_reusable_workflow_boundary |
| Trigger abuse | .github/workflows/trigger-abuse.yml |
cicd_trigger_abuse |
| Secret exfiltration | .github/workflows/secret-exfiltration.yml |
cicd_secret_exfiltration_path |
| Action supply-chain exfiltration | .github/workflows/action-supply-chain-exfiltration.yml |
cicd_action_supply_chain_exfiltration |
| Artifact/cache execution | .github/workflows/artifact-cache-poisoning.yml |
cicd_untrusted_artifact_execution |
| Suspicious workflow execution | .github/workflows/suspicious-workflow-execution.yml |
cicd_suspicious_remote_execution, cicd_encoded_payload_execution, cicd_malware_signature |
| Reusable workflow injection | .github/workflows/reusable-workflow-injection.yml |
cicd_reusable_workflow_injection |
| Environment protection bypass | .github/workflows/environment-protection-bypass.yml |
cicd_environment_protection_bypass |
| OIDC trust exposure | .github/workflows/oidc-trust-exposure.yml |
cicd_oidc_trust_exposure |
| Step summary exfiltration | .github/workflows/step-summary-exfiltration.yml |
cicd_step_summary_exfiltration |
| Dashboard rollup data-quality regression | .github/workflows/dashboard-rollup-regression.yml |
existing workflow classes with raw row, grouped row, and reachable/path-backed count separation |
| Workflow persistence | .github/workflows/workflow-persistence.yml |
cicd_workflow_persistence |
| Concurrency TOCTOU | .github/workflows/concurrency-toctou.yml |
cicd_concurrency_toctou |
| Poutine-derived all-secrets exposure | .github/workflows/poutine-all-secrets-exposure.yml |
cicd_secret_authority_exposure |
| Poutine-derived debug logging exposure | .github/workflows/poutine-debug-logging-exposure.yml |
cicd_step_summary_exfiltration, cicd_secret_exfiltration_path |
Poutine-derived malformed if: fail-open |
.github/workflows/poutine-malformed-if-fail-open.yml |
cicd_auth_logic_error, cicd_secret_authority_exposure |
| Poutine-derived bot auto-merge confused deputy | .github/workflows/poutine-bot-auto-merge-confused-deputy.yml |
cicd_auth_logic_error, cicd_untrusted_checkout |
| Scorecard-derived dangerous workflow script | .github/workflows/scorecard-dangerous-script-injection.yml |
cicd_command_injection |
| Scorecard-derived default token authority | .github/workflows/scorecard-default-write-token.yml |
cicd_auth_logic_error, cicd_trigger_abuse |
| Scorecard-derived token permission matrix | .github/workflows/scorecard-token-permission-matrix.yml |
cicd_secret_authority_exposure with explicit permission-state evidence |
| Scorecard-derived untrusted checkout | .github/workflows/scorecard-untrusted-checkout-pr-head.yml |
cicd_untrusted_checkout |
| Scorecard-derived build component and posture context | .github/workflows/scorecard-build-component-posture-context.yml |
cicd_secret_authority_exposure with build-component, dependency-update, SBOM, and posture evidence |
| Scorecard-derived pinned dependency download-execute | .github/workflows/scorecard-pinned-dependencies-download-execute.yml |
cicd_secret_authority_exposure with unverified download-execute component evidence |
| GitLab local include traversal | .gitlab-ci.yml including .gitlab-includes/deploy.yml |
cicd_secret_authority_exposure attributed to the included deploy file |
| Inventory-only Azure/Tekton adapters | azure-pipelines.yml and .tekton/pipeline.yaml |
no native finding expected until source-to-sink semantics are fixture-proven |
| Defended controls | .github/workflows/defended-internal-release.yml, .github/workflows/defended-guarded-pr.yml, .github/workflows/defended-explicit-readonly-token.yml, .github/workflows/defended-untrusted-checkout.yml, and .github/workflows/defended-verified-download-execute.yml |
no native finding expected |
The Poutine-derived and Scorecard-derived fixtures are original synthetic workflows built from the accepted REACHABLE coverage-gap list. They are benchmark cases for source-to-sink graph proof and detector coverage; no upstream Poutine or Scorecard test code is copied into this repository. The bot auto-merge fixture uses inert simulated merge commands and models bot identity as an insufficient trust boundary when dependency-source provenance is not proven.
Run a fresh local scan and validate the metadata-backed native workflow-security rows:
scan_dir="$(mktemp -d)"
reachctl scan /path/to/reach-workflow-security-testbed --ci \
--dashboard \
--output "$scan_dir" \
--metadata-out "$scan_dir/metadata.json"
python ci/validate-workflow-security-results.py \
--metadata "$scan_dir/metadata.json" \
--repo-root /path/to/reach-workflow-security-testbedExpected result:
- workflow-security scanning finds the expected deterministic classes and subclasses
- workflow-security findings remain production/security-relevant even though
the files are under
.github/workflows - only
workflow-path:*findings enterworkflow_security_review; non-path native/tool candidates stay out of AI review and cannot becomeREACHABLEwithout source-to-sink graph proof - optional tool health for
zizmorandactionlintis reported separately from native coverage - zero-native helper and defended fixtures remain clean
- cloud/dashboard rollups keep raw workflow rows, grouped workflow rows, and reachable/path-backed counts distinct
- cloud/dashboard rollups must not include raw workflow YAML, AI prompt bodies, or secret values
- the validator prints
Workflow-security expected-results validation passed
The corpus is intentionally compact, but each fixture maps to a real remediation pattern:
- never interpolate untrusted event fields directly into shell or script contexts
- avoid
pull_request_targetfor untrusted code paths unless authorization gates, checkout boundaries, and permissions are explicit - keep release/package/cloud authority out of low-trust workflow paths
- declare explicit least-privilege token permissions on external triggers
- verify artifact provenance or digest before privileged consumption
- avoid
secrets: inheritunless caller trust is explicit and bounded - pass only explicitly scoped secrets, never wildcard secret objects
- keep secrets out of debug logs, annotations, and UI-visible channels
- treat bot identity as metadata, not as dependency-source provenance or merge authorization by itself
- make authorization
if:expressions syntactically valid and fail closed - pin third-party actions to immutable commit SHAs
- avoid self-hosted runners for untrusted external events
This repository is the isolated workflow-security lab. It keeps the full subclass corpus, chained-pipeline fixtures, reusable-workflow boundary cases, remediation-oriented experiments, and defended controls used for native workflow-security regression proof.
Copyright (c) 2026 Sthenos Security. All rights reserved.