Skip to content

Azure DevOps Pipelines provider

Parses an azure-pipelines.yml from disk, no network calls, no ADO personal access token.

Producer workflow

# --azure-path is auto-detected when azure-pipelines.yml is present at cwd;
# the CLI announces the pick on stderr.
pipeline_check --pipeline azure

# …or pass it explicitly.
pipeline_check --pipeline azure --azure-path azure-pipelines.yml

All other flags (--output, --severity-threshold, --checks, --standard, …) behave the same as with the other providers.

Shape coverage

The walker handles every layout ADO supports:

  • Flat single-job pipeline, top-level steps:
  • Single-stage multi-job, top-level jobs:
  • Multi-stage, stages: → jobs: → steps:
  • Deployment jobs, steps under strategy.{runOnce|rolling|canary}.{preDeploy|deploy|routeTraffic|postRouteTraffic}.steps and strategy.*.on.{success|failure}.steps.

What it covers

38 checks · 11 have an autofix patch (--fix).

Check Title Severity Fix
ADO-001 Task reference not pinned to specific version HIGH 🔧 fix
ADO-002 Script injection via attacker-controllable context HIGH
ADO-003 Variables contain literal secret values CRITICAL
ADO-004 Deployment job missing environment binding MEDIUM
ADO-005 Container image not pinned to specific version HIGH
ADO-006 Artifacts not signed MEDIUM
ADO-007 SBOM not produced MEDIUM
ADO-008 Credential-shaped literal in pipeline body CRITICAL 🔧 fix
ADO-009 Container image pinned by tag rather than sha256 digest LOW
ADO-010 Cross-pipeline download: ingestion unverified CRITICAL
ADO-011 template: <local-path> on PR-validated pipeline HIGH
ADO-012 Cache@2 key derives from $(System.PullRequest.*) MEDIUM
ADO-013 Self-hosted pool without explicit ephemeral marker MEDIUM
ADO-014 AWS auth uses long-lived access keys MEDIUM 🔧 fix
ADO-015 Job has no timeoutInMinutes, unbounded build MEDIUM 🔧 fix
ADO-016 Remote script piped to shell interpreter HIGH 🔧 fix
ADO-017 Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
ADO-018 Package install from insecure source HIGH 🔧 fix
ADO-019 extends: template on PR-validated pipeline points to local path CRITICAL
ADO-020 No vulnerability scanning step MEDIUM
ADO-021 Package install without lockfile enforcement MEDIUM 🔧 fix
ADO-022 Dependency update command bypasses lockfile pins MEDIUM 🔧 fix
ADO-023 TLS / certificate verification bypass HIGH 🔧 fix
ADO-024 No SLSA provenance attestation produced MEDIUM
ADO-025 Cross-repo template not pinned to commit SHA HIGH
ADO-026 Pipeline contains indicators of malicious activity CRITICAL
ADO-027 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
ADO-028 Package install bypasses registry integrity (git / path / tarball source) MEDIUM
ADO-029 Service-connection-using job without environment or branch gate HIGH
ADO-030 pool interpolates attacker-controllable value HIGH 🔧 fix
ADO-031 Secret variable echoed / printed in a script step HIGH
ADO-032 checkout persistCredentials leaves the pipeline token in .git/config HIGH
ADO-033 IaC apply on a PR-validated pipeline CRITICAL
ADO-034 ML model loaded with trust_remote_code (code execution) HIGH
ADO-035 Untrusted PR/commit context reaches an agentic AI CLI (prompt injection) HIGH
ADO-036 Unsafe deserialization of a fetched artifact (pickle RCE) HIGH
ADO-037 AI model pulled without a pinned revision MEDIUM
ADO-038 Agentic CLI output lands without human review HIGH

ADO-001: Task reference not pinned to specific version

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

Floating-major task references (@1, @2) can roll forward silently when the task publisher ships a breaking or malicious update. Pass when every task: reference carries a two- or three-segment semver.

Recommended action

Reference tasks by a full semver (DownloadSecureFile@1.2.3) or extension-published-version. Track task updates explicitly via Azure DevOps extension settings rather than letting @1 drift.

ADO-002: Script injection via attacker-controllable context

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

$(Build.SourceBranch*), $(Build.SourceVersionMessage), and $(System.PullRequest.*) are populated from SCM event metadata the attacker controls. Inline interpolation into a script body executes crafted content.

The rule inspects:

  • Script bodies from the script: / bash: / pwsh: / powershell: shorthands.
  • Inline scripts in a task's inputs.script (Bash@3 / PowerShell@2 / CmdLine@2).
  • Compile-time template injection: a free-form string parameter (no values: allowlist) spliced into a script via ${{ parameters.X }}, which becomes pipeline structure before any quoting applies.

Recommended action

Pass these values through an intermediate pipeline variable declared with readonly: true, and reference that variable through an environment variable rather than $(...) macro interpolation. ADO expands $(…) before shell quoting, so inline use is never safe.

ADO-003: Variables contain literal secret values

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

Scans variables: in both the mapping form ({KEY: VAL}) and the list form ([{name: X, value: Y}]) that ADO supports. AWS keys are detected by value shape regardless of variable name.

Recommended action

Store secrets in an Azure Key Vault or a Library variable group with the secret flag set; reference them via $(SECRET_NAME) at runtime. For cloud access prefer Azure workload identity federation.

ADO-004: Deployment job missing environment binding

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

Without an environment: binding, ADO cannot enforce approvals, checks, or deployment history against a named resource. Every deployment: job should bind one.

Known false-positive modes

  • The deploy-name regex (deploy / release / publish / promote) flags jobs whose names include those tokens for non-deploy reasons (e.g. release-notes-build that only generates a changelog). The deploy-command regex similarly fires on test pipelines that exercise kubectl apply --dry-run or helm template for validation. Suppress those jobs per-resource via --ignore-file once you've verified they don't actually mutate any environment.

Recommended action

Add environment: <name> to every deployment: job. Configure approvals, required branches, and business-hours checks on the matching Environment in the ADO UI.

ADO-005: Container image not pinned to specific version

HIGH CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-TRUSTED-REG CWE-829

Container images can be declared at resources.containers[].image or job.container (string or {image:}). Floating / untagged refs let the publisher swap the image contents.

Recommended action

Reference images by @sha256:<digest> or at minimum a full immutable version tag. Avoid :latest and untagged refs.

ADO-006: Artifacts not signed

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

Passes when cosign / sigstore / slsa-* / notation-sign appears anywhere in the pipeline text.

Recommended action

Add a task that runs cosign sign or notation sign, Azure Pipelines' workload identity federation enables keyless signing. Publish the signature to the artifact feed and verify it at deploy time.

ADO-007: SBOM not produced

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

Without an SBOM, downstream consumers can't audit the dependency set shipped in the artifact.

Recommended action

Add an SBOM step, microsoft/sbom-tool, syft . -o cyclonedx-json, or anchore/sbom-action. Publish the SBOM as a pipeline artifact so downstream consumers can ingest it.

ADO-008: Credential-shaped literal in pipeline body

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

Complements ADO-003 (which looks at variables: keys). ADO-008 scans every string in the pipeline against the cross-provider credential-pattern catalog.

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 Azure Key Vault or a secret variable group and reference it via $(SECRET_NAME).

ADO-009: Container image pinned by tag rather than sha256 digest

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

ADO-005 fails floating tags at HIGH; ADO-009 is the stricter tier. Even immutable-looking version tags can be repointed by registry operators.

Recommended action

Resolve each image to its current digest and replace the tag with @sha256:<digest>. Schedule regular digest bumps via Renovate or a scheduled pipeline.

ADO-010: Cross-pipeline download: ingestion unverified

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

resources.pipelines: declares an upstream pipeline; a download: <name> step pulls its artifacts. If the upstream accepts PR validation, the artifact may have been built by PR-controlled code.

Recommended action

Add a verification step before consuming the artifact: cosign verify-attestation, sha256sum -c, or gpg --verify against a manifest the producing pipeline signed.

ADO-011: template: <local-path> on PR-validated pipeline

HIGH CICD-SEC-4 ESF-D-INJECTION ESF-S-PIN-DEPS CWE-78

template: <relative-path> includes another YAML from the CURRENT repo. On PR validation builds, the repo content is the PR branch, letting the PR author swap the template body. Cross-repo templates (template: foo.yml@my-repo) are version-pinned and not affected.

Recommended action

Move the template into a separate, branch-protected repository and reference it via template: foo.yml@<repo-resource> with a pinned ref: on the resource. That way the template content is fixed at PR creation time and can't be modified from the PR branch.

ADO-012: Cache@2 key derives from $(System.PullRequest.*)

MEDIUM CICD-SEC-4 ESF-D-INJECTION ESF-S-VERIFY-DEPS CWE-345

Cache@2 (and older CacheBeta@1) restore by key. A key including PR-controlled variables on PR-validated pipelines lets a PR seed a poisoned cache entry that a later default-branch pipeline restores.

Recommended action

Build the cache key from values the PR can't control: $(Agent.OS), lockfile hashes, the pipeline name. Never reference $(System.PullRequest.*) or $(Build.SourceBranch*) from a cache key namespace.

ADO-013: Self-hosted pool without explicit ephemeral marker

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

pool: { name: <agent-pool> } (or the bare string form pool: <name>) targets a self-hosted agent pool. Without an explicit ephemeral arrangement, agents reuse state across jobs. Microsoft-hosted pools (vmImage: or the Azure Pipelines / Default names) are skipped.

Recommended action

Configure the agent pool with autoscaling + ephemeral agents (the Azure VM Scale Set agent), and add demands: [ephemeral -equals true] on the pool block so this check can verify it.

ADO-014: AWS auth uses long-lived access keys

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

Long-lived AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY values in pipeline variables or task inputs can't be rotated on a fine-grained schedule. Prefer OIDC or vault-based credential injection for cross-cloud access.

Known false-positive modes

  • The check only flags literal AKIA-shaped values, never variable names. The residual false positive is a literal AKIA-shaped value that is actually a deactivated or test key (a documentation sample, or a deliberately revoked credential left in place). The rule can't tell a live key from a dead one. Suppress per-pipeline via --ignore-file once you've confirmed the value is deactivated or non-production.

Recommended action

Use workload identity federation or an Azure Key Vault task to inject short-lived AWS credentials at runtime. Remove static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from pipeline variables and task parameters.

ADO-015: Job has no timeoutInMinutes, unbounded build

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

Without timeoutInMinutes, the job runs until Azure's 60-minute default kills it. Explicit timeouts cap blast radius and the window during which a compromised step has access to service connections.

Recommended action

Add timeoutInMinutes: to each job, sized to the 95th percentile of historical runtime plus margin. Azure's default is 60 minutes, an explicitly shorter value limits blast radius and agent cost.

ADO-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 pipeline. 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.

ADO-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 pipeline 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.

ADO-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 pipeline. 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.

ADO-019: extends: template on PR-validated pipeline points to local path

CRITICAL CICD-SEC-4 ESF-D-INJECTION ESF-S-PIN-DEPS CWE-78

extends: template: <local-file> includes another YAML from the CURRENT repo. On PR validation builds, the repo content is the PR branch, letting the PR author swap the template body and inject arbitrary pipeline logic. Cross-repo templates (template: foo.yml@my-repo) are version-pinned and not affected.

Recommended action

Pin the extends template to a protected repository ref (template@ref). Local templates in PR-validated pipelines can be poisoned by the PR author.

ADO-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 common scanners including trivy, grype, snyk, pip-audit, osv-scanner, govulncheck, semgrep, checkov, and others.

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.

ADO-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.

ADO-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).

ADO-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.

ADO-024: No SLSA provenance attestation produced

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

On Azure Pipelines the common pattern is a Bash@3 task invoking cosign attest --yes --predicate=provenance.json $(image). The native Microsoft SBOM tool emits _manifest/spdx_2.2/manifest.spdx.json for SBOM but does not produce provenance on its own.

Recommended action

Add a task that runs cosign attest against a provenance.intoto.jsonl statement, or Microsoft's sbom-tool in attestation mode. ADO-006 covers signing; this rule covers the in-toto statement SLSA Build L3 additionally requires.

ADO-025: Cross-repo template not pinned to commit SHA

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

Azure Pipelines resolves template: build.yml@tools against the tools repo resource's ref: field. When that ref is refs/heads/main (or missing, which defaults to the pipeline's default branch), a push to the callee repo changes what your pipeline runs on the next invocation.

Recommended action

On every resources.repositories entry referenced from a template: ...@repo-alias directive, set ref: refs/tags/<sha> or the bare 40-char commit SHA, never a branch or floating tag. A moved branch/tag swaps the template body without changing your pipeline file.

ADO-026: Pipeline contains indicators of malicious activity

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

ADO pipelines can run arbitrary shell via bash / script / powershell tasks. This rule scans every string value for known-bad patterns (reverse shells, base64-decoded execution, miner binaries, exfil channels). Orthogonal to ADO-016/ADO-017/ADO-023.

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 PR/branch that added the matching task(s), rotate any Service Connections the pipeline can reach, and audit Pipeline run logs for outbound traffic to the matched hosts.

ADO-027: Dangerous shell idiom (eval, sh -c variable, backtick exec)

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

Complements ADO-002 (script injection from untrusted PR context). 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

  • 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 that must feed a dynamic command at the boundary.

ADO-028: Package install bypasses registry integrity (git / path / tarball source)

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

Complements ADO-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 (Azure Artifacts) instead of installing from a filesystem path or tarball URL.

ADO-029: Service-connection-using job without environment or branch gate

HIGH CICD-SEC-2 CWE-284

Pairs with IAM-008 (the AWS-side OIDC rule). Azure's equivalent trust path runs through service connections that map to Azure AD federated identity credentials. The ADO-side gate is either a deployment + environment or a branch-pinned condition; this rule flags jobs that have neither.

Recommended action

Every job that consumes an Azure service connection (via AzureCLI@, AzurePowerShell@, AzureKeyVault@, AzureWebApp@, etc.) must either be a deployment: job bound to an environment: (which carries approval checks and audit) or carry a condition: that pins Build.SourceBranch to a protected ref. Without one of those gates, any branch push drives the federated assume-role on Azure AD.

ADO-030: pool interpolates attacker-controllable value

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

ADO-013 catches self-hosted pools that aren't ephemeral; this rule catches the upstream targeting choice. When pool: (or its name / demands sub-fields) is computed from an attacker-controllable expression, whoever triggers the pipeline picks where the job runs, including any agent pool the project exposes (deploy-prod, signer, hsm …). Two attacker surfaces are flagged: runtime SCM macros ($(Build.SourceBranchName), $(System.PullRequest.SourceBranch), …) and caller-controlled template parameters (${{ parameters.X }}, the value comes from whoever queued the run). The rule walks all three pool shapes, string scalar, dict { name, vmImage, demands }, and the demands list form.

Known false-positive modes

  • Pipelines that intentionally select agent pools via a vetted variables: block (POOL_NAME: prod-pool) are out of scope, pipeline variables defined in the same file are author-controlled. Static custom names are not flagged. The rule only matches the curated runtime-macro catalog and the literal ${{ parameters.X }} template-parameter shape.

Recommended action

Hard-code pool: to a specific agent pool name (or vmImage: for Microsoft-hosted). If pool selection has to be parameterized, validate the candidate against an explicit allowlist before the job runs (e.g. a condition: guard against a vetted set), and never inline $(Build.*) / $(System.PullRequest.*) / ${{ parameters.X }} values as the pool name or as a demand.

ADO-031: Secret variable echoed / printed in a script step

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

Scans script:, bash:, powershell:, and pwsh: step bodies. Azure template expressions $(VAR) are matched alongside POSIX $VAR / ${VAR} forms.

Variables declared with issecret: true in the pipeline YAML are treated as known secrets (highest confidence). Variables whose names match common secret patterns (PASSWORD, TOKEN, API_KEY, etc.) are flagged heuristically.

Recommended action

Don't print secret values in pipeline scripts. Azure Pipelines masks variables marked issecret=true in logs, but only exact-match substrings. Encoded, truncated, or derived forms bypass the mask, and raw API log downloads are not masked. Log a boolean instead. Avoid set -x when secret-bound variables are in scope.

ADO-032: checkout persistCredentials leaves the pipeline token in .git/config

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

The Azure analogue of the GitHub persist-credentials / ArtiPACKED leak (GHA-037). Fires on any checkout step (checkout: self / a repository resource) that sets persistCredentials: true. The persisted token survives in .git/config for the rest of the job, so a later git config --get http.<host>.extraheader (or attacker-controlled build code) recovers it. Both the bare boolean and the quoted-string form are matched.

Recommended action

Drop persistCredentials: true from checkout steps (the default is false). When set, Azure Pipelines writes the System.AccessToken (the pipeline's OAuth token) into .git/config as an AUTHORIZATION: bearer extraheader after fetch, where every later step, including code from an untrusted PR, can read and reuse it to push or reach other repos. If a step genuinely needs to push back, scope a dedicated credential to that step instead of persisting the ambient token for the whole job.

ADO-033: IaC apply on a PR-validated pipeline

CRITICAL CICD-SEC-4 ESF-D-INJECTION CWE-94 CWE-78

Fires when a pipeline declares PR validation (pr: set to anything other than none / false) and any script: / bash: / pwsh: / powershell: step (or a task's inputs.script) runs an IaC apply command. A pr:-validated pipeline runs the PR branch's YAML and scripts, so the apply executes untrusted IaC. This is the Azure DevOps analog of GHA-117 / GL-041 / BB-033. A pipeline with no pr: key (or pr: none) is out of scope, matching ADO-011 / ADO-019.

Known false-positive modes

  • A pipeline that runs apply only against a short-lived, fully-sandboxed review environment with no production-adjacent credentials. Even then the apply executes unreviewed IaC on the agent; prefer plan on PR validation. Suppress with a rationale naming the sandbox scope.

Recommended action

Never run terraform apply (or cloudformation deploy / cdk deploy / pulumi up / sam deploy / terragrunt apply) in a pipeline that opts into PR validation (pr:). The PR branch's IaC executes at apply time, so an external data source, a local-exec provisioner, or a hijacked provider runs arbitrary code on the agent with whatever cloud credentials (often a federated service connection) the apply uses, before the change is reviewed or merged. On PR validation run a read-only plan; move the apply onto the default-branch (trigger:) leg, gated by a protected environment:.

ADO-034: 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 step's script / bash / pwsh / powershell body or a task-based step's inputs.script. 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 on the agent with its 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 job scoped to no production service connections, and prefer safetensors weights over pickle.

ADO-035: Untrusted PR/commit context reaches an agentic AI CLI (prompt injection)

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

The AI analog of ADO-002 (script injection). Fires when a step's script body (script / bash / pwsh / powershell or a task-based step's inputs.script) invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) AND attacker-controllable Azure context reaches it, either an untrusted macro ($(Build.SourceVersionMessage)) interpolated directly, or a variables: entry whose value carries one. Unlike a shell, an LLM ingests a quoted / env-routed value as prompt text, so the ADO-002 mitigation does not apply, which is why this is separate.

Recommended action

Do not place attacker-controllable context (a PR's commit message, source branch, or $(System.PullRequest.*) metadata) in an agentic CLI's prompt. Routing through a quoted env: variable 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 on a job with no service-connection secrets and no tool / shell access, and treat its output as untrusted.

ADO-036: Unsafe deserialization of a fetched artifact (pickle RCE)

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

Fires per script body (script / bash / pwsh / powershell or a task-based step's inputs.script) in two shapes (shared with GHA-122 / GL-047 via _primitives/unsafe_deser): an explicit unsafe opt-in (weights_only=False / allow_pickle=True) always; or a remote fetch (curl / wget / hf_hub_download / snapshot_download / huggingface-cli download / requests) alongside a pickle-backed loader (torch.load / pickle.load(s) / joblib.load) with no safe path (weights_only=True or safetensors) in the same body. A bare local load with no fetch does not fire.

Recommended action

Load models / artifacts through a non-executing format: prefer safetensors, or pass weights_only=True to torch.load (default in PyTorch 2.6+). Never pickle.load / joblib.load / numpy.load(allow_pickle=True) a file fetched at build time, and pin + checksum any model you must deserialize.

ADO-037: 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 step script / bash / pwsh / powershell body (or a task-based step's inputs.script) that fetches a model by a mutable registry reference and supplies no revision pin. 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 command (revision='<sha>' / --revision <sha>), when the reference is a local path (./model, /models/x) or a variable interpolation (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 (ADO-034) and prefer safetensors weights over pickle.

ADO-038: Agentic CLI output lands without human review

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

Fires when one job both invokes an agentic CLI (claude / gemini / cursor-agent / aider / openhands / goose / q chat) in a step body (script / bash / pwsh / powershell or a task-based step's inputs.script) and, in the same job, lands the result with no review gate. The landing command is one of: an az repos pr create / update carrying --auto-complete (Azure Repos merges the PR once policies pass), or a plain git push (committing straight to a branch). Coupling is per job because the steps of one Azure job share a checkout.

Does NOT fire when the agent only opens a pull request for review (az repos pr create with no --auto-complete), nor on a push / auto-complete job that does not run an agent (ordinary formatting / generated-file bots). The agent-plus-auto-land coupling is the signal. A git push --dry-run is ignored.

Known false-positive modes

  • A job that runs an agent for a read-only task (triage, labeling) but also pushes an unrelated generated file would match by co-location. Split the agent and the push into separate jobs, or suppress on the job with a rationale noting the agent does not write the pushed paths.

Recommended action

Don't let an agentic CLI's output reach a branch or a merge without a human review gate. Have the agent open a normal pull request (az repos pr create with no --auto-complete) so a person reviews the diff before it lands; drop --auto-complete from the agent's job, and don't pair the agent with a git push straight to a branch. If the agent's prompt can be influenced by untrusted input (a PR commit message, a fetched page), treat the committed result as attacker-controlled.


Adding a new Azure DevOps Pipelines check

  1. Create a new module at pipeline_check/core/checks/azure/rules/adoNNN_<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/azure/ADO-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 azure