vlotpipe rules¶
Every rule vlotpipe ships, one page each: what it checks, why, a flagged example and a fixed one, and how to suppress it if you need to. All examples on these pages are original — written for this documentation, not reproduced from any other tool's docs or advisories.
vlotpipe supports two platforms: GitHub Actions
(.github/workflows/*.yml) and Azure Pipelines
(azure-pipelines.yml, any directory depth). Most rules are GitHub-only
(marked below); a handful of structural checks, PERF001, and SEC016
are platform-neutral and run against both; AZR* codes are Azure-only. See
docs/AZURE_RESEARCH.md for why several
GitHub-only rules — SEC001 (no SHA-pin equivalent for Azure tasks),
SEC006 (Azure's checkout already defaults the safe way), LEAN002
(Azure's shallow-fetch default isn't visible in the YAML) — deliberately
don't have Azure counterparts.
Rule codes follow <CATEGORY><NNN>. Categories:
- YAML — pipeline-aware YAML style and syntax, independent of any policy: is it valid, is it internally consistent — the "yamllint for pipelines" half of vlotpipe, see below
- SEC — GitHub Actions security (secrets, supply-chain integrity, injection, token scope)
- AZR — Azure Pipelines-specific (its own security/reliability concerns)
- SUPPLY — supply-chain tooling (keeping pinned dependencies current) — GitHub-only
- PERF — runtime performance (caching, concurrency, redundant work)
- LEAN — pipeline files and build times staying short
- TIMEOUT — job/step time bounds — GitHub-only (see
AZR001for Azure) - STRUCT — pipeline structure and completeness — platform-neutral
- DUP — near-duplicate jobs, possibly across different files — platform-neutral; see
DUP001.md. Unlike every other category, aDUP001finding isn't arules.Violationunder the hood and doesn't appear in--format json/github/azure-devopsoutput — see the page for why.
Why a YAML category exists¶
Generic yamllint has no notion of GitHub Actions or Azure Pipelines
schema, which cuts both ways: it misses pipeline-specific context, and
it also produces false noise a pipeline-aware checker doesn't have to.
The clearest example is yamllint's own truthy rule, which by default
flags GitHub Actions' own required on: trigger key as an ambiguous
boolean-like key — real projects carry a custom yamllint config just to
silence that one key. YAML005 only ever inspects mapping values,
never keys, so on: is never a candidate to begin with; nothing to
special-case. See internal/yamllint/
for the implementation — these checks run on the raw YAML tree directly
rather than the normalized internal/model, since duplicate keys and
quoting style exist below the level the model captures, and apply
identically regardless of platform.
Every finding can be suppressed either per-repo in .vlotpipe.yml or
inline with a # vlotpipe: ignore[CODE] comment — see each page's
"Suppressing" section, or the main README.
Not on this list yet — write your own via custom_rules: in
.vlotpipe.yml, no fork or rebuild required.
A rule marked Autofix: yes below can be applied automatically with
vlotpipe scan/check --fix — see
docs/GETTING_STARTED.md
for what that does and doesn't cover. Only five rules have one; the rest
need a human call.
Every rule can also be tuned per repo via .vlotpipe.yml's rules:
section, keyed by exact code — two properties are generic and apply to
every rule the same way, documented here once rather than on each page:
severity: blocker|warning|info— replaces the rule's shipped severity everywhere it's consulted (display, thecheckgate, andreport.to), not just how it's colored in text output.fix: false— for a rule that's normally autofixable, skip the automatic edit only; the finding still fires and gets reported as usual.
A handful of rules also take a special property, meaningful only to
that one rule — those are documented on that rule's own page instead
(STRUCT002.max_steps, PERF001/LEAN010.cached_runners,
TIMEOUT001/AZR001.fix_default). See
docs/adr/0002-rule-specific-runner-config.md
for the full reasoning behind the split.
YAML¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| YAML001 | blocker | file is not valid YAML | |
| YAML002 | warning | duplicate key in the same mapping (silently overwritten, not merged) | |
| YAML003 | info | trailing whitespace | |
| YAML004 | info | missing or extra newline(s) at end of file | |
| YAML005 | info | unquoted YAML 1.1 boolean-like value (yes/no/on/off/y/n) |
Security¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| SEC001 | blocker | action not pinned to a full commit SHA | |
| SEC002 | blocker | hardcoded credential in with/env |
|
| SEC003 | blocker | pull_request_target/workflow_run/issue_comment checks out the untrusted head |
|
| SEC004 | blocker | untrusted input interpolated directly into run: |
|
| SEC005 | warning | no permissions: set at workflow or job level |
|
| SEC006 | warning | yes | actions/checkout without persist-credentials: false |
| SEC007 | warning | secrets: inherit on a reusable workflow call |
|
| SEC008 | warning | toJSON(secrets) dumps the entire secrets context |
|
| SEC010 | warning | self-hosted-looking runner in a fork-triggerable workflow | |
| SEC011 | warning | spoofable github.actor == authorization check |
|
| SEC012 | blocker | GITHUB_ENV/GITHUB_PATH write with untrusted input on a dangerous trigger |
|
| SEC013 | warning | re-enabled deprecated, injectable workflow commands | |
| SEC014 | blocker | hardcoded container/service registry credentials | |
| SEC015 | warning | cache restored inside a release-triggered workflow | |
| SEC016 | warning | step dumps the entire environment to the log (GitHub + Azure) |
Azure Pipelines¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| AZR001 | warning | yes | job with no timeoutInMinutes set (defaults to 60 min on Microsoft-hosted agents) |
| AZR002 | blocker | hardcoded credential in a task's inputs:/env: |
Supply chain¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| SUPPLY001 | warning | no Dependabot/Renovate config to keep pinned actions updated |
Performance¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| PERF001 | warning → info on self-hosted-looking runners | dependency install without a matching cache step (GitHub + Azure) | |
| PERF002 | info | yes | no concurrency: group on a pull_request-triggered workflow |
| PERF003 | info | action wraps a CLI already available on the runner |
Lean pipelines¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| LEAN001 | warning | installs a language runtime via the package manager every run (GitHub + Azure) | |
| LEAN002 | info | fetch-depth: 0 with no step that appears to need git history |
|
| LEAN008 | info | two jobs share the same first 3 steps, copy-pasted instead of extracted (GitHub + Azure) | |
| LEAN010 | warning → info on self-hosted-looking runners | Docker build step with no cache-from/cache-to backend | |
| LEAN011 | info | yes | uploaded artifact with no retention-days set |
Timeouts & structure¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| TIMEOUT001 | warning | yes | job with no timeout-minutes set |
| STRUCT001 | info | no job name suggests a test/lint/check step runs (GitHub + Azure) | |
| STRUCT002 | info | job has more than 20 steps (configurable) — pipeline bloat/tidiness (GitHub + Azure) |
DUP¶
| Code | Severity | Autofix | Checks |
|---|---|---|---|
| DUP001 | info | job is a near-duplicate (≥90% structural similarity) of another job found in this scan (GitHub + Azure) |
Research behind these rules¶
- The
YAMLcategory follows the well-established conventions of yamllint, the standard generic YAML linter —duplicate-key,trailing-spaces,new-line-at-end-of-file, andtruthyare all yamllint rules by name. What's different here is scope, not novelty:YAML005only inspects values, never keys, so it doesn't need the config carve-out real projects add to silence yamllint's default flagging of GitHub Actions' ownon:key. docs/SECURITY_RESEARCH.md— theSEC/SUPPLYcategory research: OWASP's CI/CD Top 10, zizmor's audit catalog, and real incidents (tj-actions/changed-files, ArtiPACKED) behind each choice.docs/LEAN_PIPELINES_RESEARCH.md— theLEAN/PERFcategory research.docs/AZURE_RESEARCH.md— what ports from GitHub Actions to Azure Pipelines and what doesn't, with the verified Azure YAML schema facts behind eachAZR*rule.docs/TROPHY_CASE.md— real bugs found and fixed by running vlotpipe against real pipelines (SEC016itself came out of one of those runs).