Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit
Project description
proofctl
A linter built for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. 264 rules, zero dependencies, runs in under a second.
proofctl check .
PROOFCTL-S-001 module/auth.py:42 SQL injection via string format in .execute() ERROR
PROOFCTL-Q-001 module/models.py:17 Mutable default argument — list assigned at def time ERROR
PROOFCTL-S-AI-006 module/loaders.py:88 Empty `except Exception:` swallows all errors silently WARNING
PROOFCTL-TF-A024 infra/sg.tf:14 SG rule 0.0.0.0/0 ingress on port 22 ERROR
PROOFCTL-YAML-022 k8s/deployment.yaml:8 Workload uses default namespace WARNING
PROOFCTL-YAML-007 .github/workflows/release.yml:23 Action not pinned to SHA ERROR
PROOFCTL-SECRET-001 infra/lambda.tf:8 AWS access key (AKIA...) ERROR
──────────────────────────────────────────────────────────────────
7 findings (4 ERROR, 3 WARNING) — exit 2
Why proofctl
LLM coding assistants ship code that compiles and looks correct but fails in predictable ways:
- Insecure — SQL/command injection, hardcoded credentials, weak crypto, JWT signature bypass, public S3 buckets, K8s default namespaces.
- Broken — hallucinated imports, phantom methods, type-hint vs implementation mismatch,
pass-bodied functions shipped as real implementations. - Fragile — broad
except Exception:, no timeouts, mutable defaults, defensive guards for impossible states, unused configuration kwargs. - Wasteful — single-method classes that should be functions, near-duplicate files, sprawling kwargs surface.
proofctl runs as a pre-commit linter and catches these patterns before they merge. The rule set is measured, not just intuited — see proofctl-eval/BENCHMARK.md for per-rule signal-lift numbers across:
- 17 paired AI-vs-human prompts (Layer D)
- 788 real AI-attributed commits across 107 individual hobbyist Python repos + 153 individual infrastructure repos (Layer E)
- 42 hand-labeled slop scenarios (Layer F)
The top-firing rules on real AI-authored production code are Q-002 broad except, Q-005 complexity, Q-006 long functions, YAML-007/GHA-006 GitHub Actions hygiene, and YAML-GHA-003 missing permissions — confirmed across both synth and real-world corpora.
What proofctl scans
| Language | What it catches |
|---|---|
| Python | Security (SQLi, injection, eval, JWT bypass, weak crypto), quality (mutable defaults, broad except, complexity, long functions), AI-slop patterns (echo docstrings, single-method classes, stale kwargs), import hallucinations |
| Terraform / HCL | AWS / GCP / Azure resource hardening (200+ rules across IAM, encryption, public exposure, audit logs), AI-slop patterns (deprecated resources, AWS region in GCP, placeholder values), required_version, sensitive outputs |
| Kubernetes YAML | Pod Security Standards (runAsNonRoot, allowPrivilegeEscalation, readOnlyRootFilesystem, drop ALL caps, seccomp), data-exfiltration vectors (default namespace, hostPath, low UID, RBAC wildcards, automountServiceAccountToken), image hygiene (latest tag, digest pinning) |
| Helm / Mustache | Same K8s checks via Helm-templated YAML |
| Dockerfile | USER, base image pinning, secret leakage, root-running, OCI labels, HEALTHCHECK, AI-generated antipatterns |
| GitHub Actions | SHA-pinning, expression injection, missing permissions, missing timeouts, secret env-var leakage |
| Shell | Common mistake detection (set -euo, dangerous globbing) |
| Secrets (any text file) | AWS / GCP / GitHub / Slack / Stripe / private keys / JWTs / hardcoded passwords / K8s Secret YAML / HCL heredocs |
Installation
pip install proofctl
Requires: Python 3.11+
No project dependencies are added — proofctl only scans, never imports your code.
Quick start
proofctl check . # scan current directory
proofctl check src/api/auth.py # scan a single file
proofctl check . --min-severity ERROR # only show ERROR
proofctl check . --fail-on ERROR # CI gate: exit non-zero only on ERROR
proofctl check . --changed-only # only files changed in this branch
proofctl check . --families S,Q,SECRET # restrict rule families
proofctl check . --format html --output report.html
proofctl rules # list all rules
Pre-commit integration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/kolawoluu/proofctl
rev: v0.3.1
hooks:
- id: proofctl
Or local:
repos:
- repo: local
hooks:
- id: proofctl
name: proofctl
language: system
entry: proofctl check
args: [--no-pypi, --fail-on, ERROR]
pass_filenames: false
CI integration (GitHub Actions)
name: proofctl
on: [push, pull_request]
jobs:
proofctl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install proofctl
# --new-only compares against the committed baseline snapshot —
# CI fails only on regressions introduced in this PR.
- run: proofctl check . --no-pypi --fail-on ERROR --new-only
- name: Generate HTML report
if: failure()
run: proofctl check . --no-pypi --format html --output proofctl-report.html || true
- uses: actions/upload-artifact@v4
if: failure()
with:
name: proofctl-report
path: proofctl-report.html
retention-days: 14
Create a baseline snapshot for an existing project so CI only flags regressions:
proofctl baseline .
git add .proofctl-baseline.json
git commit -m "chore: add proofctl baseline"
Rules — 255 total across 14 families
Python — Quality (17 rules)
| Rule | Severity | Catches |
|---|---|---|
| Q-001 | ERROR | Mutable default argument |
| Q-002 | ERROR/WARNING | Bare or broad except with no re-raise (top AI-slop wedge — measured) |
| Q-003 | WARNING | Excessive Any or unexplained # type: ignore |
| Q-004 | INFO | Single-implementation abstraction |
| Q-005 | WARNING/ERROR | Cyclomatic complexity (>10 / >20) |
| Q-006 | WARNING/ERROR | Function body too long (>50 / >100 lines) |
| Q-007 | WARNING/ERROR | Too many parameters (>5 / >7) |
| Q-008 | WARNING | Boolean literal as positional argument |
| Q-009 | INFO | print() in non-test code |
| Q-AI-001 | WARNING | Confabulated enum error message ("must be one of [a,b,c]" without membership check) |
| Q-AI-002 | ERROR | Return-type annotation contradicts return value (-> int: return f"x: {x}") |
| Q-AI-003 | WARNING | Stale parameter not forwarded to inner call |
| Q-AI-004 | INFO | Single-method class (over-engineered abstraction) |
| Q-AI-005 | WARNING | Defensive guard for impossible state |
| Q-AI-006 | WARNING/ERROR | except Exception: pass — generic exception swallow |
| Q-AI-007 | INFO | Sprawling unused kwargs (4+ defaulted, ≥2 unreferenced) |
| Q-AI-008 | INFO | Verbose echo docstring on trivial accessor |
Python — Security (24 rules)
| Rule | Severity | Catches | Authority |
|---|---|---|---|
| S-001 | ERROR | SQL injection via string formatting | OWASP A03, CWE-89 |
| S-002 | ERROR | Command injection (shell=True) |
OWASP A03, CWE-78 |
| S-003 | ERROR | Unsafe deserialization | OWASP A08, CWE-502 |
| S-004 | ERROR | Weak crypto (md5/sha1 — hashlib.md5, from hashlib import md5, Crypto.Hash.MD5.new()) |
OWASP A02, CWE-327 |
| S-005 | ERROR | eval()/exec() on non-literal input |
CWE-95 |
| S-006 | WARNING | Missing timeout= on HTTP calls |
CWE-400 |
| S-007 | ERROR | JWT signature bypass | OWASP A07 |
| S-008 | ERROR | Path traversal — extends to os.remove, shutil.*, tarfile, zipfile, archive extractall |
CWE-22 |
| S-009 | ERROR | Insecure UUID for security tokens | CWE-338 |
| S-010 | WARNING | Info exposure — traceback.format_exc() / str(e) in responses + secrets in logs |
OWASP A09, CWE-200 |
| S-011 | ERROR/WARNING | Insecure cipher mode (ECB, ARC4, TripleDES) + RSA/DSA keysize <2048 + static IV | OWASP A02 |
| S-012 | WARNING | Regex injection / ReDoS | CWE-625 |
| S-013 | WARNING | Open redirect — Flask/Django/FastAPI redirect() + Location header from user input |
CWE-601 |
| S-014 | ERROR | TLS verification disabled (verify=False) |
OWASP A02, CWE-295 |
| S-015 | WARNING | random.* used for security-sensitive value |
CWE-338 |
| S-016 | WARNING/INFO | Subprocess with partial executable path | CWE-426 |
| S-017 | ERROR | XSS — request taint into HTML sinks (Flask f-string return, HTMLResponse, mark_safe/Markup) |
OWASP A03, CWE-79 |
| S-018 | ERROR | SSRF — request taint into HTTP-client URLs (requests/httpx/urllib) | OWASP A10, CWE-918 |
| S-019 | WARNING | Unrestricted file upload — Flask request.files.save() without secure_filename + extension allowlist |
OWASP A04, CWE-434 |
| S-020 | ERROR | Insufficiently protected creds — password in URL / HTTP basic auth / plaintext into DB | OWASP A02, CWE-522 |
| S-021 | WARNING | Incorrect file permissions — `chmod(0o777 | 0o666), umask(0)` |
| S-022 | ERROR | XPath injection — f-string / % / + into xpath() / lxml.etree.XPath |
OWASP A03, CWE-643 |
| S-023 | WARNING | CSRF — @csrf_exempt on POST handler |
OWASP A01, CWE-352 |
| S-024 | WARNING | Missing authorization — admin/internal/delete paths without auth decorator | OWASP A01, CWE-862 |
Secrets (13 rules) — scans every text file
| Rule | Severity | Catches |
|---|---|---|
| SECRET-001 | ERROR | AWS access key (AKIA...) |
| SECRET-002 | ERROR | AWS secret key |
| SECRET-003 | ERROR | GitHub token (ghp_*, gho_*, ghs_*) |
| SECRET-004 | ERROR | Slack token (xoxb/p/o/a-...) |
| SECRET-005 | ERROR | Private key marker (-----BEGIN ... PRIVATE KEY-----) |
| SECRET-006 | WARNING | JWT token |
| SECRET-007 | ERROR | Generic high-entropy password=/secret=/api_key= assignment — Python AST visitor catches kwargs in calls, module-level *_PASSWORD = "...", tuple-auth auth=("u", "p") |
| SECRET-008 | ERROR | Google API key (AIza...) |
| SECRET-009 | ERROR | Stripe key (sk_live_*, pk_live_*) |
| SECRET-010 | ERROR | Hashicorp Terraform admin password (administrator_login_password) |
| SECRET-011 | ERROR | kind: Secret YAML with non-empty data: / stringData: |
| SECRET-012 | ERROR | Secret patterns inside HCL heredocs (user_data = <<EOF) |
| SECRET-013 | ERROR | Connection string with embedded credentials (postgres://user:pass@host, mysql, redis, mongodb, amqp, sqlserver) |
Python — Other (12 rules)
| Family | Rules | What it covers |
|---|---|---|
| Placeholder (P) | P-001..003 | Unimplemented functions, TODOs, commented-out code |
| Leakage (L) | L-001..003 | JS-in-Python idioms, PII patterns, cross-language slop |
| Imports (I) | I-001, I-002 | Hallucinated imports, high-risk packages |
| Methods (M) | M-001 | Method too complex |
| Variants (V) | V-001, V-002 | Near-duplicate functions / files |
| LLM Guardrails (LLM) | LLM-001..005 | OWASP LLM Top-10 (prompt injection, max_tokens, agent loops) |
Terraform — General + AI-slop (15 + 11 rules)
TF-T001..T015: required_version constraints, sensitive outputs, hardcoded secrets, wildcard IAM, mutable git refs, etc.
TF-AI-001..011: placeholder values, AWS region in GCP, deprecated resource types (aws_alb → aws_lb), inline lifecycle blocks removed in provider v4, etc. (AI-specific patterns no other linter looks for)
Terraform — AWS (40 rules)
TF-A001..A040. Highlights:
- A001 EC2 IMDSv2 not enforced
- A002 S3 missing public-access-block
- A007 EBS not encrypted
- A012 VPC missing flow logs
- A015 KMS rotation not enabled
- A016 IAM policy with wildcard Resource/Action
- A017–A023 RDS hardening (encryption, IAM auth, deletion protection, audit logs)
- A024 SG rule with
0.0.0.0/0ingress on sensitive ports - A025 EKS public endpoint without CIDR restriction
- A027 Neptune cluster unencrypted
- A029 DynamoDB without
server_side_encryption - A030 ElastiCache transit/at-rest encryption disabled
- A034 IAM policy attached directly to user
- A035 IAM role with wildcard Principal
- A036/A037 CloudTrail validation / multi-region
- A038 Default SG with non-empty rules
- A039 Subnet auto-assigns public IP
- A040 Neptune cluster without CMK
Terraform — GCP (50+ rules)
TF-G001..G046. Highlights:
- G001..G016 GKE hardening (auto-repair, node pool config, network policy, master auth networks, binary auth)
- G017–G019 GCS public access prevention, uniform access, versioning
- G020
google_service_account_key(key in TF state) - G021/G026 Broad IAM roles /
allUsersIAM bindings - G023..G025 KMS rotation, prevent_destroy, destroy window
- G027/G028 Cloud Function / Cloud Run with public access
- G029 Subnet missing VPC Flow Logs
- G030 BigQuery dataset with public access
- G031 Pub/Sub without CMEK
- G032/G033 GCS access logging, private_ip_google_access
- G034..G041 GKE deep (legacy ABAC, private nodes, metadata server, secure boot, default SA)
- G042..G046 Compute firewall public ingress, Cloud SQL SSL, public IPs, IP forwarding
Terraform — Azure (14 rules)
TF-AZ001..AZ014. Storage public blob, SQL public network, SSL enforcement, AKS IP/RBAC, Key Vault purge protection, role wildcards, App Service HTTPS, VM encryption-at-host, NSG SSH/RDP from internet, managed disk CMK.
Terraform — Mechanics + Lifecycle (12 rules)
TF-M001..M007: SG inline-vs-rule mixing, force_destroy, prevent_destroy, mutable module refs, etc.
TF-V001..V005: lifecycle antipatterns.
Terragrunt (6 rules)
TG-001..TG-006: var.* in HCL inputs, missing mock_outputs, invalid if_exists, unencrypted remote state.
Dockerfile (15 rules)
DF-001..DF-010 plus AI-specific DF-AI-002..005. Covers latest tags, root user, secret leakage, --no-install-recommends, OCI labels, HEALTHCHECK, legacy ENV KEY VALUE form.
Kubernetes YAML (26 rules)
YAML-001..018, 022..026. Pod Security Standards, hostPath, default namespace, image digest pinning, runAsUser, RBAC wildcards, NET_RAW. YAML-AI-001..006 covers removed K8s API versions and deprecated annotations.
GitHub Actions (YAML-GHA-*)
SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts. Top wedge family on infrastructure repos per BENCHMARK.md — AI tools writing CI workflows consistently miss these.
Shell (8 rules)
SH-001..008. Dangerous globbing, missing set -euo pipefail, etc.
For a complete machine-readable list:
proofctl rules
proofctl rules --family TF
proofctl rules --severity ERROR
Suppressing findings
Inline (single line):
result = subprocess.run(cmd, shell=True) # proofctl: ignore[PROOFCTL-S-002]
resource "aws_s3_bucket" "logs" {
acl = "public-read" # proofctl: ignore[PROOFCTL-TF-AI-003] // or # comment style
}
Project-wide in .proofctl.yaml:
disable:
- PROOFCTL-Q-004
Configuration
Place .proofctl.yaml in the project root:
exclude_paths:
- "**/.venv/**"
- "**/migrations/**"
- "**/.terraform/**"
disable:
- PROOFCTL-Q-004
- PROOFCTL-DF-AI-002
severity_overrides:
PROOFCTL-P-002: ERROR # promote TODOs to ERROR
rules:
PROOFCTL-P-002:
allowed_formats:
- 'TODO\(#\d+\)' # allow TODO(#123) format
exclude_paths:
- tests/
PROOFCTL-Q-003:
any_threshold: 5
PROOFCTL-V-002:
similarity_threshold: 0.85
min_file_lines: 30
PROOFCTL-I-001:
local_namespaces:
- mycompany_
- internal_
PROOFCTL-TF-T013:
required_labels:
- environment
- team
- cost_center
Commands
proofctl check
proofctl check [PATH] [OPTIONS]
Options:
--format / -f terminal | json | html (default: terminal)
--output / -o Write report to file
--changed-only Only scan git-changed files
--families P,Q,S,L,I,M,V,LLM,TF,TG,DF,YAML,SH,SECRET
--min-severity INFO | WARNING | ERROR
--fail-on Exit non-zero at this severity (default: WARNING)
--no-pypi Skip PyPI lookups (faster, works offline)
--new-only Suppress findings present in .proofctl-baseline.json
--fix Auto-fix fixable findings (Q-001 mutable defaults)
proofctl baseline
Snapshot the current findings so future --new-only runs only surface regressions.
proofctl baseline .
git add .proofctl-baseline.json && git commit -m "chore: proofctl baseline"
proofctl rules
proofctl rules # list all
proofctl rules --family S # filter by family
proofctl rules --severity ERROR
Exit codes
| Code | Meaning |
|---|---|
0 |
No findings |
1 |
Findings present, none at or above --fail-on threshold |
2 |
One or more findings at or above --fail-on threshold |
Output formats
Terminal (default) — rich-coloured table with file:line, rule, severity, hint.
JSON — machine-readable for CI:
{
"schema_version": 1,
"summary": { "total": 4, "ERROR": 3, "WARNING": 1 },
"findings": [
{
"file": "module/auth.py",
"line": 42,
"col": 8,
"rule_id": "PROOFCTL-S-001",
"rule_name": "SQL injection",
"severity": "ERROR",
"message": "SQL injection via string format in .execute()",
"hint": "Use parameterised queries: cursor.execute(sql, params)",
"authority": "OWASP A03:2021, CWE-89"
}
]
}
HTML — self-contained single-file dashboard with severity summary cards and a filterable table.
How proofctl rules earn their place
Rules in proofctl don't ship on intuition. The companion repo proofctl-eval measures every rule's per-rule signal lift — fire rate on AI-generated code divided by fire rate on human-written reference code — across three corpora:
- Layer D Synthesised: 17 paired prompts × Claude Sonnet 4.5 vs hand-written reference. Cheap to refresh (~$0.10), useful for catching anti-signal patterns.
- Layer E Real: 788 real AI-attributed commits across 107 individual hobby Python repos + 153 individual infrastructure repos. Diff-against-parent gives clean attribution of what AI introduced. The gold-standard corpus.
- Layer F Goats: 42 hand-labeled scenarios where we know which rules should fire. Recall test bed.
A rule with lift ≥ 5× is an AI-slop wedge — it fires disproportionately on AI output. A rule with lift < 1× fires more on humans (anti-signal — usually indicates a bug). The benchmark is rerun on every release; rules with collapsed lift get demoted, anti-signal rules get debugged.
Top measured wedges on real production AI commits (Layer E, May 2026):
| Rule | What it catches | Real-world fires |
|---|---|---|
| Q-002 | Broad except handler | +7 / 200 commits |
| Q-005 | Cyclomatic complexity | +6 |
| Q-006 | Function too long | +5 |
| Q-007 | Too many parameters | +4 |
| YAML-007 | GHA action not SHA-pinned | +7 |
| YAML-GHA-006 | GHA missing timeout-minutes | +7 |
| YAML-GHA-003 | GHA missing/permissive permissions | +6 |
| Q-AI-006 | Generic exception swallow | +3 |
| Q-AI-004 | Single-method class | +1 |
See BENCHMARK.md for full numbers and methodology.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file proofctl-0.4.0.tar.gz.
File metadata
- Download URL: proofctl-0.4.0.tar.gz
- Upload date:
- Size: 245.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b79440855415f67b9ec08573d09321f664795873608a198f067038bbe784abda
|
|
| MD5 |
c39e36cd16e30e89b2c6f4f8be3390a7
|
|
| BLAKE2b-256 |
d9265e4489aa1dc4bbd4716d66b06c2574cde6c5b1d91c65db020ff5ed850637
|
Provenance
The following attestation bundles were made for proofctl-0.4.0.tar.gz:
Publisher:
publish.yml on kolawoluu/proofctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
proofctl-0.4.0.tar.gz -
Subject digest:
b79440855415f67b9ec08573d09321f664795873608a198f067038bbe784abda - Sigstore transparency entry: 1488647879
- Sigstore integration time:
-
Permalink:
kolawoluu/proofctl@be9056c6fc75f03b27d4e3f98ad0d4d8428ebac5 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/kolawoluu
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@be9056c6fc75f03b27d4e3f98ad0d4d8428ebac5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file proofctl-0.4.0-py3-none-any.whl.
File metadata
- Download URL: proofctl-0.4.0-py3-none-any.whl
- Upload date:
- Size: 175.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c4f5fd295a9c24a2685395f729d2732194e7bc57561d10d7b10df1fd8aa7ad4
|
|
| MD5 |
510d305c60f520b42f57fe6e307b8b08
|
|
| BLAKE2b-256 |
a9ddaf52076c67a4767fd4a7f35e2945848e1bd47a3ea6463bdd887eee00a437
|
Provenance
The following attestation bundles were made for proofctl-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on kolawoluu/proofctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
proofctl-0.4.0-py3-none-any.whl -
Subject digest:
1c4f5fd295a9c24a2685395f729d2732194e7bc57561d10d7b10df1fd8aa7ad4 - Sigstore transparency entry: 1488647921
- Sigstore integration time:
-
Permalink:
kolawoluu/proofctl@be9056c6fc75f03b27d4e3f98ad0d4d8428ebac5 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/kolawoluu
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@be9056c6fc75f03b27d4e3f98ad0d4d8428ebac5 -
Trigger Event:
push
-
Statement type: