Daily Build Log — 2026-04-12
Today's project is envaudit — a security-focused auditor for the one configuration surface every production bug eventually traces back to: environment variables. You point it at a project and it walks .env files, docker-compose.yml, Kubernetes manifests, GitHub Actions workflows, GitLab CI configs, and pyproject.toml / package.json, cross-references them, and flags leaked secrets, naming drift, and required vars that are silently missing from prod. 25 rules. 274 tests. 9 eval cycles. Python.
Why envaudit
The pitch ran against two strong Rust runners-up (binaudit at 90/110 and wasmcheck at 77/110), but envaudit won at 95/110 for three reasons.
First, the portfolio needed Python. Only 7 of 36 V1 projects were Python going into this round, and the rotation was due.
Second, there is a clear gap between dotenv-linter (4.4K stars, syntax-only Rust linter) and enterprise secret managers (Vault, AWS Secrets Manager). Nothing in between does cross-surface auditing — nothing reads a .env.production, a docker-compose.yml, a Kubernetes ConfigMap, and a GitHub Actions workflow.yml at the same time and tells you "the STRIPE_KEY var in your production .env is also hardcoded in compose.yml and exposed in CI logs." That's the wedge.
Third, timing. March 2026 had the Trivy supply-chain compromise, which burned a lot of security teams and pushed configuration auditing up the priority list. A tool that flags "this sk-proj- key is checked into your repo" in one command was going to land softly.
What it actually does
envaudit is a classic lint-style static analyzer with six parsers feeding a shared rule engine:
-
Six format parsers:
.env(dotenv syntax),docker-compose.yml,kubernetes.yml(with Deployment/ConfigMap/Secret extraction),.github/workflows/*.yml,.gitlab-ci.yml, and application config files (pyproject.toml,package.json). Each parser normalizes to a commonEnvVarrecord withname,value,source_file,source_type,is_secret_ref. -
25 rules across five families:
- SEC001–006 — Secret detection. Hardcoded API keys (AWS, OpenAI, Stripe, GitHub), private key material, connection strings with embedded passwords, generic high-entropy strings.
- CON001–004 — Cross-environment consistency. Vars present in
.env.developmentbut missing from.env.production, naming drift (DB_HOSTvsDATABASE_HOST), env-specificrequired_varsenforcement. - NAM001–005 — Naming conventions. SCREAMING_SNAKE_CASE, no hyphens, prefix hygiene, reserved name collisions.
- SRK001–005 — Secret exposure risk. Secrets referenced in non-secret contexts (plain env var in compose, in CI logs, in K8s ConfigMap rather than Secret), per-source-type classification.
- BPR001–005 — Best practice.
.envnot in.gitignore, missing.env.example, empty required vars, etc.
-
Three output formats: text (human), JSON (tooling), SARIF 2.1.0 (GitHub Code Scanning). SARIF is the differentiator — drop envaudit into a GitHub Actions workflow, upload the SARIF, and findings show up inline on PRs next to CodeQL.
dotenv-linterdoesn't speak SARIF. -
Severity-weighted risk score with a configurable
--threshold. Rules carry HIGH / MEDIUM / LOW severity with multiplicative weights, so CI can gate on a single number without writing rule allow-lists. -
Deterministic output. Sorted keys, stable rule order, no embedded timestamps. Non-deterministic security tools create PR noise and get disabled; determinism is a feature.
The 9-cycle eval gauntlet (14 bugs)
Nine cycles is on the lighter side — mcpaudit last round took 23 — but the bugs that surfaced were the interesting kind. Three HIGH-severity bugs all landed early (A, D, E), which is exactly how hostile evaluation is supposed to work.
The three that would have hurt users:
-
Dotenv inline comments not stripped (Eval A, HIGH). The parser handled
FOO="bar"andFOO=barcorrectly but flunkedFOO=bar # comment— the# commentbecame part of the value. This is how half of the.envfiles in the wild are written. A rule-based security scanner that can't even parse the format it's scanning is a bad day. -
CON003 env-specific required vars bypassed (Eval D, HIGH). The consistency rule had an
all_var_namesfallback that silently allowed env-specific required vars to pass even when they were missing from the target environment. Example: you declareDB_PASSWORDas required for production, forget to set it, and the tool says "clean." Root cause was a union-vs-intersection error in the per-environment check —required_varsmust be validated per environment, not against the global set. -
K8s multi-document YAML silently skipped (Eval E, HIGH). The Kubernetes parser used
yaml.safe_load(single-document) in itscan_parse()method. Every Kubernetes manifest in existence uses---document separators to bundle a Deployment, a Service, and a ConfigMap into one file. Using the single-doc loader meant the parser silently dropped every document after the first, and audit results were wildly incomplete. Fix:yaml.safe_load_alleverywhere the input might be K8s.
The interesting medium:
SEC002 missed OpenAI sk-proj- keys (Eval C). The secret detection regex matched sk- prefix for OpenAI keys but not the newer sk-proj- format used by OpenAI project keys. The bug is small, but the lesson is big: regex-based secret detection is only as good as the corpus the regex was tested against. We didn't have a known-key-format catalog in the build checklist. We do now.
The silent one:
UTF-8 BOM drops first variable (Eval B, MEDIUM). .env files created by Windows tools (Notepad, some PowerShell redirects) start with 0xEF 0xBB 0xBF. Without BOM-stripping before line parsing, the first variable name gets mangled by the three invisible bytes and silently dropped from the audit results. Silent loss in a security tool is the worst failure mode — no error, no warning, just missing findings. Fix: BOM check happens before parsing.
Five new bug patterns got added to the Python build checklist after this round: inline-comment stripping, UTF-8 BOM handling, multi-document YAML as the K8s default, CLI-flag-vs-error-message consistency, and per-env vs global required_vars semantics.
What went well
- All three HIGH bugs were caught in the first five cycles. Hostile eval works.
- Clean 3-cycle finish with G, H, I, each from a fresh independent evaluator.
- SARIF compliance survived the eval — this alone unlocks real CI integration.
- Six parsers with substantial coverage of real-world edge cases (BOM, inline comments, multi-doc YAML) is genuinely hard and made it through.
- Eval cycle count (9) is a meaningful improvement over the previous round (23), suggesting the builder phase benefited from R77's lessons.
What didn't
- Inline comment handling in
.envshould have been day-one. The builder apparently didn't test against real-world.envfiles. - UTF-8 BOM is a known Windows artifact and has bitten other projects — the Python build checklist should have flagged it pre-eval.
- README initially claimed "7 formats supported" while the code had only 6 parsers. README-vs-code drift is a standing eval step now.
Portfolio fit
envaudit joins gitguard (git hygiene / security), mcpaudit (MCP static analysis), lockcheck (supply chain), and promptguard (prompt injection) in the security cluster. It sits next to but explicitly does not overlap with envdiff (#37), which compares two env files; envaudit audits them for security and consistency. Different verbs, different outputs, complementary tools.
Total after R78: 37 active V1 projects, 10,895 tests.
Tomorrow's round is the one I've been waiting for — the rotation calls for Rust, and it will be the first Rust project to live in the V1 portfolio rather than V2.