Metadata-Version: 2.4
Name: proofctl
Version: 0.2.0
Summary: Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit
Author-email: Kolawolu Odunola <kolawolu.o@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/kolawoluu/proofctl
Project-URL: Repository, https://github.com/kolawoluu/proofctl
Project-URL: Bug Tracker, https://github.com/kolawoluu/proofctl/issues
Keywords: linter,security,terraform,kubernetes,ai,static-analysis,pre-commit
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Security
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer[all]>=0.12
Requires-Dist: rich>=13
Requires-Dist: pyyaml>=6
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-mock>=3; extra == "dev"
Dynamic: license-file

# 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, `pass` bodies 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

```bash
pip install proofctl
```

**Requires:** Python 3.11+

---

## Quick start

```bash
# 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`:

```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:

```yaml
repos:
  - repo: https://github.com/kolawoluu/proofctl
    rev: v0.1.0
    hooks:
      - id: proofctl
```

---

## CI integration (GitHub Actions)

```yaml
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:

```bash
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 disabled
- `PROOFCTL-TF-G004` — Cloud SQL with public IP
- `PROOFCTL-TF-G008` — Storage bucket with `allUsers` ACL
- `PROOFCTL-TF-G015` — GCS bucket without versioning
- `PROOFCTL-TF-G026` — IAM binding with `allUsers` / `allAuthenticatedUsers`
- `PROOFCTL-TF-G030` — BigQuery dataset accessible to `allAuthenticatedUsers`

### 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 enforced
- `PROOFCTL-TF-A003` — RDS with public access
- `PROOFCTL-TF-A011` — No CloudTrail resource in file
- `PROOFCTL-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 with `allow_blob_public_access = true`
- `PROOFCTL-TF-AZ003` — SQL server with `public_network_access_enabled = true`
- `PROOFCTL-TF-AZ005` — AKS cluster with unrestricted API server access
- `PROOFCTL-TF-AZ007` — Key Vault without purge protection
- `PROOFCTL-TF-AZ011` — App Service without `https_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:

```python
result = subprocess.run(cmd, shell=True)  # proofctl: ignore[PROOFCTL-S-002]
```

Project-wide suppression in `.proofctl.yaml`:

```yaml
disable:
  - PROOFCTL-Q-004   # YAGNI check too noisy for this repo
```

---

## Configuration

Place `.proofctl.yaml` in the project root:

```yaml
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.

```bash
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:

```json
{
  "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.

---

## License

MIT
