Skip to main content

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_albaws_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/0 ingress 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 / allUsers IAM 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proofctl-0.4.0.tar.gz (245.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

proofctl-0.4.0-py3-none-any.whl (175.6 kB view details)

Uploaded Python 3

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

Hashes for proofctl-0.4.0.tar.gz
Algorithm Hash digest
SHA256 b79440855415f67b9ec08573d09321f664795873608a198f067038bbe784abda
MD5 c39e36cd16e30e89b2c6f4f8be3390a7
BLAKE2b-256 d9265e4489aa1dc4bbd4716d66b06c2574cde6c5b1d91c65db020ff5ed850637

See more details on using hashes here.

Provenance

The following attestation bundles were made for proofctl-0.4.0.tar.gz:

Publisher: publish.yml on kolawoluu/proofctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for proofctl-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1c4f5fd295a9c24a2685395f729d2732194e7bc57561d10d7b10df1fd8aa7ad4
MD5 510d305c60f520b42f57fe6e307b8b08
BLAKE2b-256 a9ddaf52076c67a4767fd4a7f35e2945848e1bd47a3ea6463bdd887eee00a437

See more details on using hashes here.

Provenance

The following attestation bundles were made for proofctl-0.4.0-py3-none-any.whl:

Publisher: publish.yml on kolawoluu/proofctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page