Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit
Project description
proofctl
Zero-dependency Python linter and infrastructure policy checker for pre-commit hooks. Detects AI-generated slop, security misconfigurations, and quality regressions across Python, Terraform (AWS/GCP/Azure), Dockerfiles, Kubernetes manifests, and GitHub Actions workflows.
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-P-001 module/tasks.py:89 Unimplemented function body (pass / ...) ERROR
PROOFCTL-TF-G004 infra/main.tf:14 GKE node pool has auto-repair disabled WARNING
──────────────────────────────────────────────────────────────────
4 findings (3 ERROR, 1 WARNING) exit 2
Why proofctl
AI coding assistants generate plausible-looking code that is frequently:
- Insecure — SQL/command injection, hardcoded secrets, weak crypto, JWT bypass
- Broken — hallucinated imports, phantom methods,
passbodies shipped as real functions - Fragile — mutable defaults, broad
except:, no timeouts, untested paths - Wasteful — near-duplicate files, single-implementation abstractions, TODO bombs
proofctl is a pre-commit linter purpose-built to catch all of this before it merges. It runs in under a second on a typical repo, requires no internet access, and adds zero runtime dependencies to your project.
Installation
pip install proofctl
Or for development:
git clone https://github.com/kolawoluu/proofctl
cd proofctl
pip install -e ".[dev]"
Requires: Python 3.11+
Quick start
# Scan the current directory
proofctl check .
# Scan a single file
proofctl check src/api/auth.py
# Only report ERROR findings
proofctl check . --min-severity ERROR
# Exit non-zero only on ERROR (useful for CI gate)
proofctl check . --fail-on ERROR
# Scan only files changed in this branch
proofctl check . --changed-only
# Run only security and quality families
proofctl check . --families S,Q
# Generate an HTML report
proofctl check . --format html --output report.html
# List all rules
proofctl rules
Pre-commit integration
Add to .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: proofctl
name: proofctl
language: system
entry: proofctl check
args: [--no-pypi, --fail-on, ERROR]
types: [python]
pass_filenames: false
Or pin from the published GitHub repo:
repos:
- repo: https://github.com/kolawoluu/proofctl
rev: v0.1.0
hooks:
- id: proofctl
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 to suppress pre-existing findings:
proofctl baseline .
git add .proofctl-baseline.json
git commit -m "chore: add proofctl baseline"
Rules
Python — Placeholder (P)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-P-001 | ERROR | Unimplemented function body (pass, ..., raise NotImplementedError) |
| PROOFCTL-P-002 | WARNING | TODO / FIXME / HACK / XXX comment present |
| PROOFCTL-P-003 | INFO | Commented-out code block |
Python — Quality (Q)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-Q-001 | ERROR | Mutable default argument (list, dict, set at def time) |
| PROOFCTL-Q-002 | ERROR | Bare except: or broad except Exception with no re-raise |
| PROOFCTL-Q-003 | WARNING | Excessive Any usage or unexplained # type: ignore |
| PROOFCTL-Q-004 | INFO | Single-implementation abstraction (YAGNI violation) |
| PROOFCTL-Q-005 | WARNING/ERROR | High cyclomatic complexity (warn >10, error >20) |
| PROOFCTL-Q-006 | WARNING/ERROR | Function body too long (warn >50 lines, error >100 lines) |
| PROOFCTL-Q-007 | WARNING/ERROR | Too many parameters (warn >5, error >7) |
| PROOFCTL-Q-008 | WARNING | Boolean literal as positional argument (flag argument) |
| PROOFCTL-Q-009 | INFO | print() in non-test code instead of logging |
Python — Security (S)
| Rule | Severity | Description | Authority |
|---|---|---|---|
| PROOFCTL-S-001 | ERROR | SQL injection via string formatting in .execute() |
OWASP A03, CWE-89 |
| PROOFCTL-S-002 | ERROR | Command injection via shell=True or os.system() |
OWASP A03, CWE-78 |
| PROOFCTL-S-003 | ERROR | Unsafe deserialization (pickle.loads, yaml.load without SafeLoader) |
OWASP A08, CWE-502 |
| PROOFCTL-S-004 | ERROR | Weak cryptographic primitive (md5/sha1 for auth, random for secrets) |
OWASP A02, CWE-327 |
| PROOFCTL-S-005 | ERROR | eval() / exec() on non-literal input |
CWE-95 |
| PROOFCTL-S-006 | WARNING | Missing timeout= on HTTP calls (requests, httpx) |
CWE-400 |
| PROOFCTL-S-007 | ERROR | JWT signature bypass (verify_signature: False, algorithms: ["none"]) |
OWASP A07 |
| PROOFCTL-S-008 | ERROR | Path traversal — open() with user-supplied path |
CWE-22 |
| PROOFCTL-S-009 | ERROR | Insecure UUID for security token (uuid1/uuid3 for token/secret/key) |
CWE-338 |
| PROOFCTL-S-010 | WARNING | Secret value leaked into logs (password/token in logging.* call) |
OWASP A09 |
| PROOFCTL-S-011 | ERROR/WARNING | Insecure cipher mode (ECB, ARC4, TripleDES) |
OWASP A02 |
| PROOFCTL-S-012 | WARNING | Regex injection — non-literal pattern passed to re.compile/re.search |
CWE-625 |
| PROOFCTL-S-013 | WARNING | Open redirect — redirect() with user-supplied URL |
CWE-601 |
Python — Leakage (L)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-L-001 | ERROR | Hardcoded secret or credential in source |
| PROOFCTL-L-002 | WARNING | PII pattern in source (email, SSN, phone number) |
| PROOFCTL-L-003 | WARNING | Cross-language idiom (JS/Java/Go pattern in Python) |
Python — Imports (I)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-I-001 | WARNING | Import from unknown or unresolvable package (hallucination) |
| PROOFCTL-I-002 | ERROR | High-risk or deprecated package (telnetlib, pickle, etc.) |
Python — Methods (M)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-M-001 | WARNING | Method too complex — abstraction opportunity |
Python — Variants (V)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-V-001 | WARNING | Near-duplicate function bodies (DRY violation) |
| PROOFCTL-V-002 | INFO | Near-duplicate file (Jaccard similarity above threshold) |
Python — LLM Guardrails (LLM)
| Rule | Severity | Description | Authority |
|---|---|---|---|
| PROOFCTL-LLM-001 | ERROR | Unsanitised user input in LLM prompt (f-string in content=) |
OWASP LLM01 |
| PROOFCTL-LLM-002 | WARNING | Missing max_tokens on LLM API call |
OWASP LLM10 |
| PROOFCTL-LLM-003 | WARNING | LLM API call inside a loop (unbounded cost) | OWASP LLM10 |
| PROOFCTL-LLM-004 | WARNING | PII variable name in LLM prompt | OWASP LLM02 |
| PROOFCTL-LLM-005 | ERROR | while True: agentic loop with LLM call and no iteration guard |
OWASP LLM06 |
Terraform — General (TF-T)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-TF-T001 | ERROR | Empty or null resource block |
| PROOFCTL-TF-T002 | WARNING | count = 0 resource |
| PROOFCTL-TF-T003 | ERROR | Hardcoded secret in resource attribute |
| PROOFCTL-TF-T004 | WARNING | Wildcard IAM permission (*) |
| PROOFCTL-TF-T005 | WARNING | lifecycle { ignore_changes = all } |
| PROOFCTL-TF-T006 | WARNING | Local-only module source (no registry or git ref) |
| PROOFCTL-TF-T007 | ERROR | Mutable git ref in module source (branch / HEAD) |
| PROOFCTL-TF-T008 | WARNING | Missing description on variable |
| PROOFCTL-TF-T009 | WARNING | Missing description on output |
| PROOFCTL-TF-T010 | WARNING | Null-default variable without validation block |
| PROOFCTL-TF-T011 | ERROR | Remote state without encryption |
| PROOFCTL-TF-T012 | WARNING | Provisioner block (prefer cloud-init or user_data) |
| PROOFCTL-TF-T013 | ERROR | Missing required resource labels |
| PROOFCTL-TF-T014 | WARNING | No terraform { required_version } constraint |
| PROOFCTL-TF-T015 | WARNING | Sensitive-named output without sensitive = true |
Terraform — GCP (TF-G)
25 rules covering GCP compute, Cloud SQL, GKE, Cloud Storage, IAM, KMS, VPC, Cloud Run, BigQuery, Pub/Sub, and Cloud Functions. Highlights:
PROOFCTL-TF-G001— GKE node pool with auto-repair disabledPROOFCTL-TF-G004— Cloud SQL with public IPPROOFCTL-TF-G008— Storage bucket withallUsersACLPROOFCTL-TF-G015— GCS bucket without versioningPROOFCTL-TF-G026— IAM binding withallUsers/allAuthenticatedUsersPROOFCTL-TF-G030— BigQuery dataset accessible toallAuthenticatedUsers
Terraform — AWS (TF-A)
14 rules covering EC2 IMDSv2, RDS, EKS, EBS, Lambda, ElastiCache, S3, ECR, CloudTrail, and VPC flow logs. Highlights:
PROOFCTL-TF-A001— EC2 without IMDSv2 enforcedPROOFCTL-TF-A003— RDS with public accessPROOFCTL-TF-A011— No CloudTrail resource in filePROOFCTL-TF-A014— Lambda function with hardcoded secret in env vars
Terraform — Azure (TF-AZ)
12 rules covering Azure storage, SQL/Postgres/MySQL, AKS, Key Vault, Monitor, RBAC, App Service, and VMs. Highlights:
PROOFCTL-TF-AZ001— Storage account withallow_blob_public_access = truePROOFCTL-TF-AZ003— SQL server withpublic_network_access_enabled = truePROOFCTL-TF-AZ005— AKS cluster with unrestricted API server accessPROOFCTL-TF-AZ007— Key Vault without purge protectionPROOFCTL-TF-AZ011— App Service withouthttps_only = true
Terraform — Mechanics (TF-M) and Lifecycle (TF-V)
7 + 4 rules covering security group mixing, ignore_changes = all, external provisioners, force_destroy, prevent_destroy, and mutable module refs.
Terragrunt (TG)
6 rules covering var.* in HCL inputs, missing mock_outputs, invalid if_exists values, and unencrypted remote state.
Dockerfile (DF)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-DF-001 | WARNING | FROM uses latest tag |
| PROOFCTL-DF-002 | ERROR | Running as root (USER root or no USER directive) |
| PROOFCTL-DF-003 | WARNING | apt-get install without --no-install-recommends |
| PROOFCTL-DF-004 | ERROR | Secret in ARG or ENV (password/token/key name) |
| PROOFCTL-DF-005 | WARNING | ADD used where COPY is sufficient |
| PROOFCTL-DF-006 | INFO | Multiple RUN commands that could be merged |
| PROOFCTL-DF-007 | ERROR | curl/wget output piped directly to shell |
| PROOFCTL-DF-008 | WARNING | ADD from URL without checksum verification |
| PROOFCTL-DF-009 | WARNING | COPY of dependency manifest without pinned install |
| PROOFCTL-DF-010 | INFO | Missing OCI image labels (org.opencontainers.image.*) |
YAML / Kubernetes (YAML-K8S)
13 rules based on Kubernetes Pod Security Standards. Highlights:
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-YAML-009 | ERROR | Container missing runAsNonRoot: true |
| PROOFCTL-YAML-010 | ERROR | Container missing allowPrivilegeEscalation: false |
| PROOFCTL-YAML-011 | WARNING | Container missing readOnlyRootFilesystem: true |
| PROOFCTL-YAML-012 | WARNING | Container missing drop: [ALL] capabilities |
| PROOFCTL-YAML-013 | WARNING | Pod missing seccompProfile annotation |
| PROOFCTL-YAML-014 | WARNING | automountServiceAccountToken: true on pod or service account |
| PROOFCTL-YAML-015 | ERROR | ClusterRoleBinding to cluster-admin |
| PROOFCTL-YAML-016 | WARNING | RBAC rule with wildcard verb or resource (*) |
| PROOFCTL-YAML-017 | ERROR | RBAC rule with escalate/impersonate/bind verb |
| PROOFCTL-YAML-018 | WARNING | Deployment/DaemonSet container missing liveness or readiness probe |
| PROOFCTL-YAML-019 | WARNING | Ingress without TLS configuration |
| PROOFCTL-YAML-020 | ERROR | Plaintext secret in environment variable (value: on secret-named var) |
| PROOFCTL-YAML-021 | WARNING | Namespace-scoped RoleBinding using cluster-admin |
YAML / GitHub Actions (YAML-GHA)
| Rule | Severity | Description |
|---|---|---|
| PROOFCTL-YAML-007 | ERROR | Expression injection — ${{ github.event.* }} in run: step |
| PROOFCTL-YAML-GHA-001 | ERROR | pull_request_target with actions/checkout of PR head ref |
| PROOFCTL-YAML-GHA-002 | WARNING | Workflow missing top-level permissions: block |
| PROOFCTL-YAML-GHA-003 | WARNING | Secret passed directly to environment variable in workflow |
| PROOFCTL-YAML-GHA-004 | ERROR | ACTIONS_ALLOW_UNSECURE_COMMANDS: true in workflow |
| PROOFCTL-YAML-GHA-005 | WARNING | Job missing timeout-minutes |
Suppressing findings
Inline suppression for a single line:
result = subprocess.run(cmd, shell=True) # proofctl: ignore[PROOFCTL-S-002]
Project-wide suppression in .proofctl.yaml:
disable:
- PROOFCTL-Q-004 # YAGNI check too noisy for this repo
Configuration
Place .proofctl.yaml in the project root:
exclude_paths:
- "**/.venv/**"
- "**/migrations/**"
- "**/.terraform/**"
disable:
- PROOFCTL-Q-004
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 Comma-separated families: P,Q,S,L,I,M,V,LLM,TF,TG,DF,YAML
--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
Print all rule IDs, names, and severities.
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, and hint.
JSON — machine-readable array for CI integrations:
{
"summary": { "total": 4, "ERROR": 3, "WARNING": 1 },
"findings": [
{
"file": "module/auth.py",
"line": 42,
"col": 8,
"rule_id": "PROOFCTL-S-001",
"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.
Architecture
proofctl is a pure Python AST linter with zero subprocess dependencies. It walks the repository with Python's pathlib, runs AST visitors for Python files, and custom regex/text parsers for Terraform HCL, Dockerfiles, and YAML. This makes it:
- Fast — typically < 1s for a 10k-line Python repo
- Offline — no network calls needed with
--no-pypi - Dependency-free — only
typer,rich,pyyamlare required at runtime
proofctl/
├── cli.py # typer commands: check, baseline, rules
├── engine.py # orchestrator: walks files, dispatches checkers
├── config.py # .proofctl.yaml loader
├── models.py # Finding dataclass + Severity enum
├── baseline.py # snapshot save/load/filter
├── fixer.py # auto-fix for Q-001
├── checkers/
│ ├── placeholders.py # P family
│ ├── quality.py # Q family
│ ├── security.py # S family (S-001 to S-013)
│ ├── leakage.py # L family
│ ├── imports.py # I family
│ ├── methods.py # M family
│ ├── variants.py # V family
│ ├── llm.py # LLM family
│ ├── terraform.py # TF/TG families (T, G, A, AZ, M, V, TG)
│ ├── dockerfile.py # DF family
│ └── yaml_checker.py # YAML/K8s/GHA families
└── reporters/
├── terminal.py
├── json_reporter.py
└── html_reporter.py
Development
pip install -e ".[dev]"
pytest tests/ -v
635 tests across all rule families. To run a specific family:
pytest tests/test_security*.py -v
pytest tests/test_terraform*.py -v
pytest tests/test_yaml*.py -v
Companion tool
shieldctl — CI-stage scanner that wraps best-of-breed external tools (checkov, tfsec, gitleaks, shellcheck, hadolint, actionlint, pip-audit) for deeper infrastructure security scanning. Run proofctl pre-commit; run shieldctl in CI.
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.1.0.tar.gz.
File metadata
- Download URL: proofctl-0.1.0.tar.gz
- Upload date:
- Size: 121.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7ba40c62ffbb5466da9f1aff7f8d08f8f5a075b13d675ec49d1fc7e55f4a2e7
|
|
| MD5 |
0aced47292460efc8932894642ca3b47
|
|
| BLAKE2b-256 |
123eae6b46ab85c7fc40f736273e68ba9ec0588ee98f2972c35ba298689d0fa9
|
Provenance
The following attestation bundles were made for proofctl-0.1.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.1.0.tar.gz -
Subject digest:
b7ba40c62ffbb5466da9f1aff7f8d08f8f5a075b13d675ec49d1fc7e55f4a2e7 - Sigstore transparency entry: 1477153981
- Sigstore integration time:
-
Permalink:
kolawoluu/proofctl@af2dab506e8dcb1fafd452f45892ce3ec9edfb0a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kolawoluu
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@af2dab506e8dcb1fafd452f45892ce3ec9edfb0a -
Trigger Event:
push
-
Statement type:
File details
Details for the file proofctl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: proofctl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 93.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bda6625084be3849885dc93415a76f4619c583829e45af62b7ee48891d2188e
|
|
| MD5 |
d214109a174473b297ebb2b2bf5619f4
|
|
| BLAKE2b-256 |
de3f4c0ee0f224a8c45151b7d91bb13d46c108e342f1fc0f5f37a39a3300d2ae
|
Provenance
The following attestation bundles were made for proofctl-0.1.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.1.0-py3-none-any.whl -
Subject digest:
1bda6625084be3849885dc93415a76f4619c583829e45af62b7ee48891d2188e - Sigstore transparency entry: 1477154093
- Sigstore integration time:
-
Permalink:
kolawoluu/proofctl@af2dab506e8dcb1fafd452f45892ce3ec9edfb0a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kolawoluu
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@af2dab506e8dcb1fafd452f45892ce3ec9edfb0a -
Trigger Event:
push
-
Statement type: