The Cordyceps vulnerability class does not affect Wardex. The internal pipeline review, however, identified three gaps that required correction.
Wardex is a solo research project. All features, hardening, and fixes derive from ongoing threat intelligence analysis and observation of patterns in third-party ecosystems. There are no external production deployments, and the code is hardened against the same patterns the tool detects in other codebases.
The Cordyceps class, documented by Novee Security, describes a systemic vulnerability pattern in GitHub Actions pipelines: workflows using privileged triggers (pull_request_target, issue_comment, workflow_run) combined with user-controlled data interpolation (branch names in shell commands, PR titles in run: blocks, comment bodies in actions/github-script).
The Novee publication prompted a full internal review of github.com/had-nu/wardex against the documented attack chains. The result: Wardex is not vulnerable to the Cordyceps pattern. The investigation, however, revealed three residual risks — none of them Cordyceps, but all relevant to supply chain integrity and CI/CD pipeline hardening.
Every workflow in the repository was reviewed against the four patterns documented by Novee. The verdict was negative on three grounds:
ci.yml uses pull_request — sandboxed by design, no secret access, no write permissions. Privileged triggers do not exist in any workflow in this repository.run: blocks, comment bodies in actions/github-script, or artifact outputs in elevated contexts — are present. The ${{ github.ref_name }} usage in docker.yml is confined to a Docker build-arg via a parameterised action.A negative Cordyceps verdict does not equate to a hardened pipeline. The review identified three gaps:
curl | bash — no integrity verificationThe release.yml pipeline installed Syft with:
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s
No version pinning, no hash verification. A compromise of the anchore/syft repository or an unreviewed change to the script on main would result in arbitrary code execution on a runner with GHCR write access and the GITHUB_TOKEN. Trivy and Cosign were already pinned. Syft was the outlier.
docker.yml published branch-triggered images to the same registry as release images (ghcr.io/had-nu/wardex), but without cosign signing — the signing step was gated on if: startsWith(github.ref, 'refs/tags/').
Any contributor with write access to the dev branch could publish an unsigned image tagged with a commit SHA, carrying no integrity attestation, no SBOM, and no SLSA provenance. Wardex itself implements wardex art14 verify and wardex accept verify to detect tampered artifacts — its own Docker image should be subject to the same level of verification.
SafePath)The action.yml passes inputs to the Wardex container without YAML-layer sanitisation. The relevant attack vector is path traversal: if the CLI does not validate paths, a Marketplace Action user can read files outside the workspace with --evidence ../../.env, write outputs to arbitrary locations with --out-file ../../.github/workflows/evil.yml, or expose runner secrets via --config /proc/self/environ on Linux runners.
The existing SafePath function in pkg/utils/path.go used filepath.Clean plus a prefix check, but did not resolve symlinks. An attacker who controls a file in a writable base directory can create a symlink pointing outside the workspace, and SafePath would accept it as valid.
The curl | bash invocation was replaced with a direct binary download and SHA-256 checksum verification:
# release.yml — after
- name: Install syft
env:
SYFT_VERSION: "1.19.0"
SYFT_SHA256: "1dec148ea36aef68a866e35528974b5dbc106ba0b545f1a262ad977d48294637"
run: |
curl -sSfL \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" \
-o syft.tar.gz
echo "${SYFT_SHA256} syft.tar.gz" | sha256sum --check --strict
tar -xzf syft.tar.gz syft
install -m 0755 syft /usr/local/bin
rm syft.tar.gz syft
sha256sum was chosen over anchore/sbom-action/download-syft deliberately: the verification step is explicit and visible in every PR diff, avoiding the opacity of a third-party action.
dev- namespace and sign all buildsTwo changes to docker.yml:
dev- namespace prefix, separating them visually from release images.# docker.yml — metadata
- name: Extract metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch,prefix=dev-
type=sha,format=long,prefix=sha-
# docker.yml — signing without tag gate
- name: Sign image with cosign keyless
run: |
cosign sign ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} --yes
pathguard — symlink-aware path validationA new pkg/cli/pathguard.go package replaces utils.SafePath with two functions: ValidateInputPath (reads) and ValidateOutputPath (writes). The key difference: filepath.EvalSymlinks runs before the containment check, closing the symlink bypass.
All 33 call sites of SafePath across 19+ files were migrated. Two additional bugs were fixed during the migration:
pkg/accept/accept.go — Append and UpdateStatus validated the path but used the original (unvalidated) path for writes. The validated path is now used for both operations.cmd/evaluate/evaluate.go — the --out-file flag was not validated before os.Create. It is now validated via ValidateOutputPath.The test suite includes 33 tests covering traversal, symlink escape, null bytes, UTF-8 validation, shell injection patterns, and pseudo-filesystem access (/proc/, /sys/, /dev/):
$ go test -race ./pkg/cli/...
ok github.com/had-nu/wardex/v2/pkg/cli 0.026s coverage: 90.9%
Eleven new commands were added in this release:
| Command | Description |
|---|---|
wardex trust list | List all keys in the trust store with status, role, and metadata. Table and JSON output. |
wardex trust show <id> | Display detailed information for a specific trust key, including revocation status and signatures. |
wardex trust verify | Verify root signature validity and key status across the entire trust store. |
wardex config show | Display configuration metadata: risk appetite, gate mode, CRA Art.14 status, state store, and SHA-256 hash. |
wardex auth status | Show trust store integrity with admin, active, and revoked key counts. |
wardex auth verify --actor | Verify actor permissions against the trust store, listing all permitted operations. |
wardex contract verify | Compute SHA-256 hash of a contract file and optionally verify against an expected hash. Shows file size and modification time. |
wardex assets inventory | Display ICT asset inventory with criticality, internet exposure, network zone, and owner. Table, JSON, and CSV output. |
wardex hmac sign | Compute HMAC-SHA256 signature for file integrity verification using a secret from the environment. |
wardex convert kev | Convert the CISA KEV catalogue to Wardex YAML format for use with evaluate. |
wardex chain seal | Create a SHA-256 chain seal of all files in a directory for integrity verification. |
The v2.2.2 release ships with a signed provenance manifest as a technology preview. The manifest contains BLAKE3 hashes of all 113 source files, signed with an Ed25519 key. This attestation was generated using the v2.3.0 provenance sub-module, currently under development.
The rationale for including provenance in a CI/CD hardening release is twofold:
*.go correctly) that was fixed before the v2.3.0 branch freeze.Starting in v2.3.0, provenance attestation will be built into the Wardex CLI itself — no separate binary, no separate workflow. The wardex provenance seal and wardex provenance verify commands will generate and validate signed manifests for any directory, with optional Bitcoin anchoring via OpenTimestamps and Ethereum/Polygon anchoring via a smart contract.
Signing public key (ed25519):
ed25519:HsD9e6BB2LlaeKODGqgWUZoflDgdUH1HWTdyWA7dGqE=Root hash (BLAKE3, 113 files):
sha256:6f972edf99f5457f8fb13668c529f4343dab7a76d20b67ea746ebdf54d910fee
# Verify source tree integrity (requires the v2.3.0 binary)
immutable-provenance verify \
--manifest provenance-manifest-v2.2.2-signed.yaml \
--dir /path/to/wardex-v2.2.2
sha256sum over anchore/sbom-action/download-syft. The explicit checksum step is deliberately visible in PR diffs. A third-party action buries the integrity check behind an opaque abstraction — the same opacity pattern the hardening was designed to eliminate.ValidateInputPath resolves symlinks and checks containment. Absolute paths are accepted if they resolve within baseDir — required for compatibility with temporary directories in tests (t.TempDir()).ValidateOutputPath accepts fs.ErrNotExist (the file does not yet exist, which is expected for writes). ValidateInputPath requires the file to exist or be resolvable. Pseudo-filesystem checks (/proc/, /sys/, /dev/) are confined to ValidateOutputPath as defence in depth.The exercise identified a policy-practice gap in the project's own release pipeline. The declared policy is SHA pinning for all external dependencies. The actual practice installed Syft via curl | bash from the main branch of anchore/syft — no hash, no verification.
Wardex is designed to detect precisely this delta in corporate environments. This release closes the delta for the project itself.
go install github.com/had-nu/wardex/v2@latest
Checksums are available in wardex_2.2.2_checksums.txt from the GitHub Release.