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}.stepsandstrategy.*.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
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
$(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
stringparameter (novalues: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
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
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-buildthat only generates a changelog). The deploy-command regex similarly fires on test pipelines that exercisekubectl apply --dry-runorhelm templatefor validation. Suppress those jobs per-resource via--ignore-fileonce 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
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
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
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
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, Stripesk_test_docs keys) are suppressed via theVENDOR_EXAMPLE_TOKENSallowlist. 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
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
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
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.*)
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
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
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-fileonce 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
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
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)
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
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
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
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
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
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 theDEP_UPDATE_REtooling 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 HIGHto 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
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
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
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
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, ortestare auto-suppressed; bare lines in a production pipeline still fire. - Defaults to LOW confidence. Filter with
--min-confidence MEDIUMto 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)
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 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 any value that must feed a dynamic command at the boundary.
ADO-028: Package install bypasses registry integrity (git / path / tarball source)
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
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
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
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
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
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
applyonly against a short-lived, fully-sandboxed review environment with no production-adjacent credentials. Even then the apply executes unreviewed IaC on the agent; preferplanon 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)
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)
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)
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
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
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
- Create a new module at
pipeline_check/core/checks/azure/rules/adoNNN_<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/azure/ADO-NNN.{unsafe,safe}.ymland add aCheckCaseentry intests/test_per_check_real_examples.py::CASES. - Regenerate this doc: