proofctl 0.5.6__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {proofctl-0.5.6 → proofctl-0.6.0}/MANIFEST.in +0 -2
  2. {proofctl-0.5.6/proofctl.egg-info → proofctl-0.6.0}/PKG-INFO +34 -27
  3. {proofctl-0.5.6 → proofctl-0.6.0}/README.md +23 -23
  4. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/__init__.py +1 -1
  5. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/baseline.py +1 -1
  6. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/dockerfile.py +5 -3
  7. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/hcl_utils.py +1 -1
  8. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/imports.py +0 -110
  9. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/leakage.py +4 -4
  10. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/quality.py +19 -10
  11. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/secrets.py +10 -2
  12. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/security.py +24 -19
  13. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/terraform.py +0 -1
  14. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/variants.py +2 -2
  15. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/yaml_checker.py +21 -25
  16. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/cli.py +13 -9
  17. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/config.py +3 -3
  18. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/engine.py +17 -19
  19. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/fixer.py +11 -13
  20. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/registry.py +6 -6
  21. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/terminal.py +2 -2
  22. {proofctl-0.5.6 → proofctl-0.6.0/proofctl.egg-info}/PKG-INFO +34 -27
  23. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/requires.txt +2 -0
  24. proofctl-0.6.0/pyproject.toml +72 -0
  25. proofctl-0.5.6/pyproject.toml +0 -48
  26. {proofctl-0.5.6 → proofctl-0.6.0}/LICENSE +0 -0
  27. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/_globmatch.py +0 -0
  28. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/__init__.py +0 -0
  29. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/base.py +0 -0
  30. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/llm_integration.py +0 -0
  31. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/placeholders.py +0 -0
  32. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/shell_checker.py +0 -0
  33. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/terragrunt.py +0 -0
  34. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/models.py +0 -0
  35. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/__init__.py +0 -0
  36. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/html_reporter.py +0 -0
  37. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/json_reporter.py +0 -0
  38. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/SOURCES.txt +0 -0
  39. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/dependency_links.txt +0 -0
  40. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/entry_points.txt +0 -0
  41. {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/top_level.txt +0 -0
  42. {proofctl-0.5.6 → proofctl-0.6.0}/setup.cfg +0 -0
@@ -11,8 +11,6 @@ prune examples
11
11
  prune build
12
12
  prune dist
13
13
  prune .github
14
- exclude AUDIT_AND_ROADMAP.md
15
- exclude AUDIT_VERIFICATION_v0.1.11.md
16
14
 
17
15
  global-exclude __pycache__
18
16
  global-exclude *.py[cod]
@@ -1,10 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofctl
3
- Version: 0.5.6
4
- Summary: Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions catches AI slop and security misconfigurations pre-commit
5
- Author-email: Kolawolu Odunola <kolawolu.o@gmail.com>
3
+ Version: 0.6.0
4
+ Summary: DevOps linter and security scanner for AI slop and misconfigurationsruns in pre-commit, CI, or post-commit; no external tools required
5
+ Author: Kolawolu Odunola
6
6
  License: MIT
7
- Keywords: linter,security,terraform,kubernetes,ai,static-analysis,pre-commit
7
+ Project-URL: Homepage, https://github.com/devopscatalyst/proofctl
8
+ Project-URL: Repository, https://github.com/devopscatalyst/proofctl
9
+ Project-URL: Changelog, https://github.com/devopscatalyst/proofctl/blob/main/HISTORY.md
10
+ Project-URL: Issues, https://github.com/devopscatalyst/proofctl/issues
11
+ Keywords: linter,security-scanner,sast,iac,secrets-detection,terraform,kubernetes,ai,static-analysis,pre-commit,ci-cd,devops
8
12
  Classifier: Development Status :: 4 - Beta
9
13
  Classifier: Environment :: Console
10
14
  Classifier: Intended Audience :: Developers
@@ -13,6 +17,7 @@ Classifier: Operating System :: OS Independent
13
17
  Classifier: Programming Language :: Python :: 3
14
18
  Classifier: Programming Language :: Python :: 3.11
15
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
16
21
  Classifier: Topic :: Software Development :: Quality Assurance
17
22
  Classifier: Topic :: Security
18
23
  Requires-Python: >=3.11
@@ -24,27 +29,29 @@ Requires-Dist: pyyaml>=6
24
29
  Provides-Extra: dev
25
30
  Requires-Dist: pytest>=8; extra == "dev"
26
31
  Requires-Dist: pytest-mock>=3; extra == "dev"
32
+ Requires-Dist: mypy>=1.8; extra == "dev"
33
+ Requires-Dist: ruff>=0.4; extra == "dev"
27
34
  Dynamic: license-file
28
35
 
29
36
  # proofctl
30
37
 
31
- 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. **269 rules**, zero dependencies, runs in under a second.
38
+ A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **267 rules**, no external tools, runs in under a second.
32
39
 
33
40
  ```
34
41
  proofctl check .
35
42
  ```
36
43
 
37
- ```
38
- PROOFCTL-S-001 module/auth.py:42 SQL injection via string format in .execute() ERROR
39
- PROOFCTL-Q-001 module/models.py:17 Mutable default argument list assigned at def time ERROR
40
- PROOFCTL-S-AI-006 module/loaders.py:88 Empty `except Exception:` swallows all errors silently WARNING
41
- PROOFCTL-TF-A024 infra/sg.tf:14 SG rule 0.0.0.0/0 ingress on port 22 ERROR
42
- PROOFCTL-YAML-022 k8s/deployment.yaml:8 Workload uses default namespace WARNING
43
- PROOFCTL-YAML-007 .github/workflows/release.yml:23 Action not pinned to SHA ERROR
44
- PROOFCTL-SECRET-001 infra/lambda.tf:8 AWS access key (AKIA...) ERROR
45
- ──────────────────────────────────────────────────────────────────
46
- 7 findings (4 ERROR, 3 WARNING) — exit 2
47
- ```
44
+ | Rule | Location | Issue | Severity |
45
+ |------|----------|-------|:--------:|
46
+ | `PROOFCTL-S-001` | `module/auth.py:42` | SQL injection via string format in `.execute()` | **ERROR** |
47
+ | `PROOFCTL-Q-001` | `module/models.py:17` | Mutable default argument — list assigned at def time | **ERROR** |
48
+ | `PROOFCTL-Q-002` | `module/loaders.py:88` | Empty `except Exception:` swallows all errors silently | WARNING |
49
+ | `PROOFCTL-TF-A024` | `infra/sg.tf:14` | SG rule `0.0.0.0/0` ingress on port 22 | **ERROR** |
50
+ | `PROOFCTL-YAML-022` | `k8s/deployment.yaml:8` | Workload uses default namespace | WARNING |
51
+ | `PROOFCTL-YAML-007` | `.github/workflows/release.yml:23` | Action not pinned to SHA | **ERROR** |
52
+ | `PROOFCTL-SECRET-001` | `infra/lambda.tf:8` | AWS access key (`AKIA...`) | **ERROR** |
53
+
54
+ > **7 findings** (4 ERROR, 3 WARNING) — exits non-zero (`exit 2`) so CI fails the build.
48
55
 
49
56
  ---
50
57
 
@@ -53,11 +60,11 @@ PROOFCTL-SECRET-001 infra/lambda.tf:8 AWS access key (AKIA...)
53
60
  LLM coding assistants ship code that compiles and looks correct but fails in predictable ways:
54
61
 
55
62
  - **Insecure** — SQL/command injection, hardcoded credentials, weak crypto, JWT signature bypass, public S3 buckets, K8s default namespaces.
56
- - **Broken** — hallucinated imports, phantom methods, type-hint vs implementation mismatch, `pass`-bodied functions shipped as real implementations.
63
+ - **Broken** — hallucinated imports, type-hint vs implementation mismatch, `pass`-bodied functions shipped as real implementations.
57
64
  - **Fragile** — broad `except Exception:`, no timeouts, mutable defaults, defensive guards for impossible states, unused configuration kwargs.
58
65
  - **Wasteful** — single-method classes that should be functions, near-duplicate files, sprawling kwargs surface.
59
66
 
60
- proofctl runs as a pre-commit linter and catches these patterns before they merge. The top-firing rules on real AI-authored code are **Q-002 broad except**, **Q-005 complexity**, **Q-006 long functions**, **YAML-007 / YAML-GHA-006 GitHub Actions hygiene**, and **YAML-GHA-003 missing permissions**.
67
+ proofctl runs in pre-commit hooks, CI pipelines, or against an existing tree, catching these patterns before they merge. The top-firing rules on real AI-authored code are **Q-002 broad except**, **Q-005 complexity**, **Q-006 long functions**, **YAML-007 / YAML-GHA-006 GitHub Actions hygiene**, and **YAML-GHA-003 missing permissions**.
61
68
 
62
69
  ---
63
70
 
@@ -165,23 +172,23 @@ git commit -m "chore: add proofctl baseline"
165
172
 
166
173
  ## Rules
167
174
 
168
- 269 rules across 14 families. Run `proofctl rules` after install to enumerate.
175
+ 267 rules across 13 families. Run `proofctl rules` after install to enumerate.
169
176
 
170
177
  | Family | Count | What it covers |
171
178
  |---|---|---|
172
179
  | Python — Quality (`Q`) | 17 | Mutable defaults, broad `except`, complexity, long functions, AI-slop patterns (echo docstrings, single-method classes, stale kwargs, defensive guards) |
173
180
  | Python — Security (`S`) | 24 | OWASP Top-10 + CWE coverage: SQLi, command injection, weak crypto, JWT bypass, path traversal, XSS, SSRF, file upload, CSRF, missing authz, XPath injection, open redirect, TLS bypass |
174
181
  | Secrets (`SECRET`) | 13 | AWS / GCP / GitHub / Slack / Stripe / private keys / JWTs / hardcoded passwords / K8s Secret YAML / connection strings with embedded creds |
175
- | Python — Other | 12 | Placeholders, leakage (cross-language idioms, PII), import hallucination, method complexity, variant detection, LLM guardrails (OWASP LLM Top-10) |
176
- | Terraform — General + AI (`TF-T`, `TF-AI`) | 29 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
177
- | Terraform — AWS (`TF-A`) | 40 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
178
- | Terraform — GCP (`TF-G`) | 52 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
182
+ | Python — Other | 15 | Placeholders, leakage (cross-language idioms, PII), import hallucination, variant detection, LLM guardrails (OWASP LLM Top-10) |
183
+ | Terraform — General + AI (`TF-T`, `TF-AI`) | 25 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
184
+ | Terraform — AWS (`TF-A`) | 36 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
185
+ | Terraform — GCP (`TF-G`) | 46 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
179
186
  | Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
180
- | Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 12 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
187
+ | Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 10 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
181
188
  | Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
182
189
  | Dockerfile (`DF`) | 15 | Latest tag, root user, secret leakage, OCI labels, HEALTHCHECK, legacy ENV form |
183
- | Kubernetes YAML (`YAML`) | 26 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
184
- | GitHub Actions (`YAML-GHA`) | | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
190
+ | Kubernetes YAML (`YAML`) | 32 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
191
+ | GitHub Actions (`YAML-GHA`) | 6 | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
185
192
  | Shell (`SH`) | 8 | Dangerous globbing, missing `set -euo pipefail` |
186
193
 
187
194
  ```bash
@@ -280,7 +287,7 @@ Options:
280
287
  --format / -f terminal | json | html (default: terminal)
281
288
  --output / -o Write report to file
282
289
  --changed-only Only scan git-changed files
283
- --families P,Q,S,L,I,M,V,LLM,TF,TG,DF,YAML,SH,SECRET
290
+ --families P,Q,S,L,I,V,LLM,TF,TG,DF,YAML,SH,SECRET
284
291
  --min-severity INFO | WARNING | ERROR
285
292
  --fail-on Exit non-zero at this severity (default: WARNING)
286
293
  --no-pypi Skip PyPI lookups (faster, works offline)
@@ -1,22 +1,22 @@
1
1
  # proofctl
2
2
 
3
- 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. **269 rules**, zero dependencies, runs in under a second.
3
+ A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **267 rules**, no external tools, runs in under a second.
4
4
 
5
5
  ```
6
6
  proofctl check .
7
7
  ```
8
8
 
9
- ```
10
- PROOFCTL-S-001 module/auth.py:42 SQL injection via string format in .execute() ERROR
11
- PROOFCTL-Q-001 module/models.py:17 Mutable default argument list assigned at def time ERROR
12
- PROOFCTL-S-AI-006 module/loaders.py:88 Empty `except Exception:` swallows all errors silently WARNING
13
- PROOFCTL-TF-A024 infra/sg.tf:14 SG rule 0.0.0.0/0 ingress on port 22 ERROR
14
- PROOFCTL-YAML-022 k8s/deployment.yaml:8 Workload uses default namespace WARNING
15
- PROOFCTL-YAML-007 .github/workflows/release.yml:23 Action not pinned to SHA ERROR
16
- PROOFCTL-SECRET-001 infra/lambda.tf:8 AWS access key (AKIA...) ERROR
17
- ──────────────────────────────────────────────────────────────────
18
- 7 findings (4 ERROR, 3 WARNING) — exit 2
19
- ```
9
+ | Rule | Location | Issue | Severity |
10
+ |------|----------|-------|:--------:|
11
+ | `PROOFCTL-S-001` | `module/auth.py:42` | SQL injection via string format in `.execute()` | **ERROR** |
12
+ | `PROOFCTL-Q-001` | `module/models.py:17` | Mutable default argument — list assigned at def time | **ERROR** |
13
+ | `PROOFCTL-Q-002` | `module/loaders.py:88` | Empty `except Exception:` swallows all errors silently | WARNING |
14
+ | `PROOFCTL-TF-A024` | `infra/sg.tf:14` | SG rule `0.0.0.0/0` ingress on port 22 | **ERROR** |
15
+ | `PROOFCTL-YAML-022` | `k8s/deployment.yaml:8` | Workload uses default namespace | WARNING |
16
+ | `PROOFCTL-YAML-007` | `.github/workflows/release.yml:23` | Action not pinned to SHA | **ERROR** |
17
+ | `PROOFCTL-SECRET-001` | `infra/lambda.tf:8` | AWS access key (`AKIA...`) | **ERROR** |
18
+
19
+ > **7 findings** (4 ERROR, 3 WARNING) — exits non-zero (`exit 2`) so CI fails the build.
20
20
 
21
21
  ---
22
22
 
@@ -25,11 +25,11 @@ PROOFCTL-SECRET-001 infra/lambda.tf:8 AWS access key (AKIA...)
25
25
  LLM coding assistants ship code that compiles and looks correct but fails in predictable ways:
26
26
 
27
27
  - **Insecure** — SQL/command injection, hardcoded credentials, weak crypto, JWT signature bypass, public S3 buckets, K8s default namespaces.
28
- - **Broken** — hallucinated imports, phantom methods, type-hint vs implementation mismatch, `pass`-bodied functions shipped as real implementations.
28
+ - **Broken** — hallucinated imports, type-hint vs implementation mismatch, `pass`-bodied functions shipped as real implementations.
29
29
  - **Fragile** — broad `except Exception:`, no timeouts, mutable defaults, defensive guards for impossible states, unused configuration kwargs.
30
30
  - **Wasteful** — single-method classes that should be functions, near-duplicate files, sprawling kwargs surface.
31
31
 
32
- proofctl runs as a pre-commit linter and catches these patterns before they merge. The top-firing rules on real AI-authored code are **Q-002 broad except**, **Q-005 complexity**, **Q-006 long functions**, **YAML-007 / YAML-GHA-006 GitHub Actions hygiene**, and **YAML-GHA-003 missing permissions**.
32
+ proofctl runs in pre-commit hooks, CI pipelines, or against an existing tree, catching these patterns before they merge. The top-firing rules on real AI-authored code are **Q-002 broad except**, **Q-005 complexity**, **Q-006 long functions**, **YAML-007 / YAML-GHA-006 GitHub Actions hygiene**, and **YAML-GHA-003 missing permissions**.
33
33
 
34
34
  ---
35
35
 
@@ -137,23 +137,23 @@ git commit -m "chore: add proofctl baseline"
137
137
 
138
138
  ## Rules
139
139
 
140
- 269 rules across 14 families. Run `proofctl rules` after install to enumerate.
140
+ 267 rules across 13 families. Run `proofctl rules` after install to enumerate.
141
141
 
142
142
  | Family | Count | What it covers |
143
143
  |---|---|---|
144
144
  | Python — Quality (`Q`) | 17 | Mutable defaults, broad `except`, complexity, long functions, AI-slop patterns (echo docstrings, single-method classes, stale kwargs, defensive guards) |
145
145
  | Python — Security (`S`) | 24 | OWASP Top-10 + CWE coverage: SQLi, command injection, weak crypto, JWT bypass, path traversal, XSS, SSRF, file upload, CSRF, missing authz, XPath injection, open redirect, TLS bypass |
146
146
  | Secrets (`SECRET`) | 13 | AWS / GCP / GitHub / Slack / Stripe / private keys / JWTs / hardcoded passwords / K8s Secret YAML / connection strings with embedded creds |
147
- | Python — Other | 12 | Placeholders, leakage (cross-language idioms, PII), import hallucination, method complexity, variant detection, LLM guardrails (OWASP LLM Top-10) |
148
- | Terraform — General + AI (`TF-T`, `TF-AI`) | 29 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
149
- | Terraform — AWS (`TF-A`) | 40 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
150
- | Terraform — GCP (`TF-G`) | 52 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
147
+ | Python — Other | 15 | Placeholders, leakage (cross-language idioms, PII), import hallucination, variant detection, LLM guardrails (OWASP LLM Top-10) |
148
+ | Terraform — General + AI (`TF-T`, `TF-AI`) | 25 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
149
+ | Terraform — AWS (`TF-A`) | 36 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
150
+ | Terraform — GCP (`TF-G`) | 46 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
151
151
  | Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
152
- | Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 12 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
152
+ | Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 10 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
153
153
  | Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
154
154
  | Dockerfile (`DF`) | 15 | Latest tag, root user, secret leakage, OCI labels, HEALTHCHECK, legacy ENV form |
155
- | Kubernetes YAML (`YAML`) | 26 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
156
- | GitHub Actions (`YAML-GHA`) | | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
155
+ | Kubernetes YAML (`YAML`) | 32 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
156
+ | GitHub Actions (`YAML-GHA`) | 6 | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
157
157
  | Shell (`SH`) | 8 | Dangerous globbing, missing `set -euo pipefail` |
158
158
 
159
159
  ```bash
@@ -252,7 +252,7 @@ Options:
252
252
  --format / -f terminal | json | html (default: terminal)
253
253
  --output / -o Write report to file
254
254
  --changed-only Only scan git-changed files
255
- --families P,Q,S,L,I,M,V,LLM,TF,TG,DF,YAML,SH,SECRET
255
+ --families P,Q,S,L,I,V,LLM,TF,TG,DF,YAML,SH,SECRET
256
256
  --min-severity INFO | WARNING | ERROR
257
257
  --fail-on Exit non-zero at this severity (default: WARNING)
258
258
  --no-pypi Skip PyPI lookups (faster, works offline)
@@ -1,4 +1,4 @@
1
- """proofctl — zero-dependency linter for AI-generated slop and security misconfig."""
1
+ """proofctl — linter and security scanner for AI slop and misconfig."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from importlib.metadata import PackageNotFoundError, version as _pkg_version
@@ -3,7 +3,7 @@
3
3
  Older versions used `(rule_id, file, message)` as the fingerprint, which broke
4
4
  the moment a message embedded numerics ("complexity 23", "17 parameters") —
5
5
  a trivial edit that nudged the number defeated the match and the finding
6
- re-surfaced as "new". The H-10 fix is to *normalise* the message before
6
+ re-surfaced as "new". The fix is to *normalise* the message before
7
7
  hashing: strip embedded numbers and quoted identifiers so semantically
8
8
  identical findings collide regardless of their printed values.
9
9
 
@@ -362,8 +362,8 @@ def _df008(instructions: list[tuple[int, str, str]], path: Path) -> list[Finding
362
362
  severity=Severity.ERROR,
363
363
  message=f"ADD fetches '{url}' without checksum verification — supply chain integrity risk",
364
364
  hint="Use ADD with --checksum=sha256:<hash>, or RUN curl ... && sha256sum -c to verify before using.",
365
- authority="OWASP CICD-SEC-6Insufficient Artifact Integrity Validation",
366
- docs_url="https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-06-Insufficient_Credential_Hygiene",
365
+ authority="OWASP CICD-SEC-9Improper Artifact Integrity Validation",
366
+ docs_url="https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-09-Improper_Artifact_Integrity_Validation",
367
367
  ))
368
368
  return findings
369
369
 
@@ -510,7 +510,9 @@ def _df_ai002(instructions: list[tuple[int, str, str]], path: Path) -> list[Find
510
510
  if instr != "ENV":
511
511
  continue
512
512
  # Legacy form has no '=' in the first token pair: ENV PATH /usr/bin
513
- if "=" not in args.split()[0] if args.split() else True:
513
+ tokens = args.split()
514
+ first_has_eq = bool(tokens) and "=" in tokens[0]
515
+ if not first_has_eq:
514
516
  if _LEGACY_ENV_RE.match(args.strip()):
515
517
  findings.append(Finding(
516
518
  file=str(path),
@@ -160,7 +160,7 @@ def _parse(lines: list[str], base: int, skip_outer: bool) -> list[HclBlock]:
160
160
  # side allowed for `<<-` indented heredocs). The previous loose
161
161
  # `rstrip("-~")` matched lines like `EOF~~~` as terminators, which
162
162
  # could prematurely close a heredoc whose body legitimately ended
163
- # with `~`. C-11 fix: strict whole-line match after .strip().
163
+ # with `~`. Fix: strict whole-line match after .strip().
164
164
  if line.strip() == heredoc_end:
165
165
  heredoc_end = None
166
166
  continue
@@ -4,8 +4,6 @@ import ast
4
4
  import functools
5
5
  import json
6
6
  import re
7
- import shutil
8
- import subprocess
9
7
  import sys
10
8
  import time
11
9
  import urllib.error
@@ -294,13 +292,6 @@ def _parse_setup_cfg(text: str) -> list[tuple[str, str | None, int]]:
294
292
  results.append((m.group(1), m.group(3) or None, lineno))
295
293
  return results
296
294
 
297
- # mypy error patterns indicating phantom method calls
298
- _MYPY_ATTR_RE = re.compile(
299
- r'^(.+):(\d+):\s+error:\s+(?:Item\s+"[^"]+"\s+of\s+)?"([^"]+)"\s+has\s+no\s+attribute\s+"([^"]+)"'
300
- r'|^(.+):(\d+):\s+error:\s+Module\s+"[^"]+"\s+has\s+no\s+attribute\s+"([^"]+)"'
301
- )
302
-
303
-
304
295
  def _normalize_pkg_name(name: str) -> str:
305
296
  return re.sub(r"[-_.]+", "-", name).lower()
306
297
 
@@ -817,104 +808,3 @@ class DependencyChecker(DirectoryChecker):
817
808
  return findings
818
809
 
819
810
 
820
- class MethodChecker(DirectoryChecker):
821
- """PROOFCTL-M-001: Phantom method calls detected by a single mypy invocation.
822
-
823
- Running mypy once on all files is orders of magnitude faster than once per
824
- file: mypy's startup and import-resolution cost is paid once, not N times.
825
- """
826
-
827
- def check(self, root: Path, py_files: list[Path]) -> list[Finding]:
828
- if not py_files:
829
- return []
830
- if not shutil.which("mypy"):
831
- # Surface the disabled state as a single INFO finding so CI users
832
- # don't silently lose phantom-method coverage.
833
- return [self._mypy_missing_finding(root)]
834
- try:
835
- return self._run_mypy(py_files)
836
- except FileNotFoundError:
837
- return [self._mypy_missing_finding(root)]
838
-
839
- def _mypy_missing_finding(self, root: Path) -> Finding:
840
- return Finding(
841
- file=str(root),
842
- line=None,
843
- col=None,
844
- rule_id="PROOFCTL-M-001",
845
- rule_name="Phantom method check disabled",
846
- severity=Severity.INFO,
847
- message=(
848
- "M-001 disabled — mypy not installed; install proofctl[dev] or "
849
- "pip install mypy to enable phantom-method detection"
850
- ),
851
- hint="pip install mypy # or: pip install proofctl[dev]",
852
- authority="mypy attr-defined — AI calls methods that don't exist",
853
- docs_url="https://mypy.readthedocs.io/en/stable/error_codes.html",
854
- )
855
-
856
- def _run_mypy(self, py_files: list[Path]) -> list[Finding]:
857
- try:
858
- result = subprocess.run(
859
- [
860
- "mypy",
861
- "--ignore-missing-imports",
862
- "--no-error-summary",
863
- "--no-pretty",
864
- *[str(p) for p in py_files],
865
- ],
866
- capture_output=True,
867
- text=True,
868
- timeout=300,
869
- )
870
- except subprocess.TimeoutExpired:
871
- return []
872
- except FileNotFoundError:
873
- raise
874
- except OSError:
875
- return []
876
-
877
- # Index scoped files for quick membership check after path normalisation.
878
- scoped = {str(p.resolve()) for p in py_files}
879
-
880
- findings = []
881
- for line in result.stdout.splitlines():
882
- m = _MYPY_ATTR_RE.match(line)
883
- if not m:
884
- continue
885
-
886
- if m.group(1):
887
- file_path, lineno_str = m.group(1), m.group(2)
888
- obj_type, attr = m.group(3), m.group(4)
889
- msg = f"'{obj_type}' has no attribute '{attr}'"
890
- else:
891
- file_path, lineno_str = m.group(5), m.group(6)
892
- attr = m.group(7)
893
- msg = f"Module has no attribute '{attr}'"
894
-
895
- # Skip findings for files outside the scan scope (e.g. installed libs).
896
- try:
897
- if str(Path(file_path).resolve()) not in scoped:
898
- continue
899
- except OSError:
900
- continue
901
-
902
- try:
903
- lineno = int(lineno_str)
904
- except ValueError:
905
- lineno = None
906
-
907
- findings.append(Finding(
908
- file=file_path,
909
- line=lineno,
910
- col=None,
911
- rule_id="PROOFCTL-M-001",
912
- rule_name="Phantom method call",
913
- severity=Severity.ERROR,
914
- message=msg,
915
- hint="Check the library's documentation for the correct method name.",
916
- authority="mypy attr-defined — AI calls methods that don't exist",
917
- docs_url="https://mypy.readthedocs.io/en/stable/error_codes.html",
918
- ))
919
-
920
- return findings
@@ -51,7 +51,7 @@ _JS_ATTR_NAMES = {
51
51
  "length": ("Use len(x) instead of .length.", "PROOFCTL-L-001"),
52
52
  }
53
53
  # Sub-set of _JS_ATTR_NAMES whose severity is governed by the
54
- # `l001_strict_idioms` toggle (see H-20).
54
+ # `l001_strict_idioms` toggle.
55
55
  _JS_FUZZY_ATTRS: frozenset[str] = frozenset({"push", "length"})
56
56
  _JS_CALL_NAMES = {
57
57
  "console": {"log", "warn", "error", "info", "debug"},
@@ -117,7 +117,7 @@ class _LeakageVisitor(ast.NodeVisitor):
117
117
  # JS: .push(), .length
118
118
  if attr in _JS_ATTR_NAMES:
119
119
  hint, rule_id = _JS_ATTR_NAMES[attr]
120
- # H-20: `.push()` / `.length` produced too many false positives
120
+ # Background: `.push()` / `.length` produced too many false positives
121
121
  # against Python objects that legitimately expose those names
122
122
  # (queues, ORM cursors, PyMongo). Demote to INFO unless the
123
123
  # caller explicitly opts into strict cross-language idiom
@@ -178,7 +178,7 @@ class _LeakageVisitor(ast.NodeVisitor):
178
178
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
179
179
  """PROOFCTL-L-002d: Java-style getter/setter pairs.
180
180
 
181
- H-21: Only flag when getX AND setX both exist for the same suffix X.
181
+ Only flag when getX AND setX both exist for the same suffix X.
182
182
  Two unrelated methods like `getInfo` + `setColor` are not a Java
183
183
  pattern; only matched pairs (`getFoo` + `setFoo`) are.
184
184
  """
@@ -233,7 +233,7 @@ class _LeakageVisitor(ast.NodeVisitor):
233
233
 
234
234
  class LeakageChecker(FileChecker):
235
235
  def __init__(self, l001_strict_idioms: bool = False) -> None:
236
- # H-20: When True, `.push()` / `.length` are reported as ERROR.
236
+ # When True, `.push()` / `.length` are reported as ERROR.
237
237
  # Default False keeps them at INFO to avoid noise on Python objects
238
238
  # that legitimately expose those names (queues, ORMs, etc.).
239
239
  self._l001_strict_idioms = l001_strict_idioms
@@ -60,7 +60,7 @@ class _Q001Visitor(ast.NodeVisitor):
60
60
  type_name = (
61
61
  type(default).__name__
62
62
  if isinstance(default, (ast.List, ast.Dict, ast.Set))
63
- else (default.func.id if isinstance(default.func, ast.Name) else "mutable")
63
+ else (default.func.id if isinstance(default, ast.Call) and isinstance(default.func, ast.Name) else "mutable")
64
64
  )
65
65
  # Sync `fixable` with fixer.py capability: the auto-fixer can only
66
66
  # rewrite single-line default expressions. Multi-line literals
@@ -128,13 +128,13 @@ class _Q002Visitor(ast.NodeVisitor):
128
128
  msg = "Bare except: catches KeyboardInterrupt, SystemExit, and all exceptions silently"
129
129
  hint = "Catch specific exceptions: except (ValueError, KeyError) as e:"
130
130
  elif is_suppressed or is_log_only:
131
- # Tier 1 (WARNING): obvious swallow — body is `pass` or a single call (likely log).
131
+ # WARNING case: obvious swallow — body is `pass` or a single call (likely log).
132
132
  rule_id = "PROOFCTL-Q-002"
133
133
  sev = Severity.WARNING
134
134
  msg = "Broad except Exception with no re-raise swallows all errors"
135
135
  hint = "Catch specific exceptions or re-raise: except Exception as e: ... raise"
136
136
  else:
137
- # Tier 2 (INFO, H-16): any other broad-except that doesn't re-raise.
137
+ # INFO case: any other broad-except that doesn't re-raise.
138
138
  # Catches the wider "swallows but does five lines of dummy recovery"
139
139
  # pattern that the WARNING tier misses.
140
140
  rule_id = "PROOFCTL-Q-002"
@@ -157,7 +157,7 @@ class _Q002Visitor(ast.NodeVisitor):
157
157
  severity=sev,
158
158
  message=msg,
159
159
  hint=hint,
160
- authority="OWASP A10:2025 Mishandling of Exceptional Conditions",
160
+ authority="CWE-703Improper Check or Handling of Exceptional Conditions",
161
161
  docs_url="https://owasp.org/www-community/vulnerabilities/Improper_Error_Handling",
162
162
  ))
163
163
  self.generic_visit(node)
@@ -247,7 +247,7 @@ def _cyclomatic_complexity(func: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
247
247
  complexity += len(node.values) - 1
248
248
  elif isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
249
249
  complexity += 1
250
- # H-17: count match/case statements toward complexity.
250
+ # count match/case statements toward complexity.
251
251
  # `match` itself adds 1 (the dispatch decision); each `case` arm adds 1
252
252
  # (each is a distinct execution path).
253
253
  elif match_node is not None and isinstance(node, match_node):
@@ -345,7 +345,7 @@ class _Q007Visitor(ast.NodeVisitor):
345
345
  # Remove self / cls for methods
346
346
  if params and params[0].arg in ("self", "cls"):
347
347
  params = params[1:]
348
- # H-18: keyword-only arguments are real parameters too — `def f(*, a, b,
348
+ # keyword-only arguments are real parameters too — `def f(*, a, b,
349
349
  # c, d, e, f, g, h)` is just as overloaded as a positional version.
350
350
  params += list(args.kwonlyargs)
351
351
  count = len(params)
@@ -599,6 +599,16 @@ class QualityChecker(FileChecker):
599
599
  v9.visit(tree)
600
600
  findings.extend(v9.findings)
601
601
 
602
+ AiVisitor: type[
603
+ _QAi001Visitor | _QAi002Visitor | _QAi003Visitor
604
+ | _QAi004Visitor | _QAi005Visitor | _QAi006Visitor
605
+ | _QAi007Visitor | _QAi008Visitor
606
+ ]
607
+ v: (
608
+ _QAi001Visitor | _QAi002Visitor | _QAi003Visitor
609
+ | _QAi004Visitor | _QAi005Visitor | _QAi006Visitor
610
+ | _QAi007Visitor | _QAi008Visitor
611
+ )
602
612
  for AiVisitor in (_QAi001Visitor, _QAi002Visitor, _QAi003Visitor):
603
613
  v = AiVisitor(path)
604
614
  v.visit(tree)
@@ -1123,8 +1133,8 @@ class _QAi005Visitor(ast.NodeVisitor):
1123
1133
  for arg, default in zip(defaulted, defaults):
1124
1134
  if isinstance(default, ast.Constant) and default.value is None:
1125
1135
  names.add(arg.arg)
1126
- for arg, default in zip(fn.args.kwonlyargs, fn.args.kw_defaults or []):
1127
- if isinstance(default, ast.Constant) and default.value is None:
1136
+ for arg, kw_default in zip(fn.args.kwonlyargs, fn.args.kw_defaults or []):
1137
+ if isinstance(kw_default, ast.Constant) and kw_default.value is None:
1128
1138
  names.add(arg.arg)
1129
1139
  # Also include any param annotated with `... | None` or `Optional[...]`.
1130
1140
  for arg in positional + list(fn.args.kwonlyargs):
@@ -1396,11 +1406,10 @@ class AbstractionChecker(DirectoryChecker):
1396
1406
  abstract_classes: dict[str, _AbstractClassInfo] = {}
1397
1407
  all_classes: dict[str, list[str]] = {} # class_name -> list of base names
1398
1408
 
1399
- # TODO(perf): re-parses every Python file from disk even though the
1409
+ # Note(perf): re-parses every Python file from disk even though the
1400
1410
  # engine already parsed each tree once for FileChecker passes. Proper
1401
1411
  # fix requires extending the DirectoryChecker API to receive parsed
1402
1412
  # trees from the engine; deferred to a separate PR.
1403
- # Audit reference: AUDIT_AND_ROADMAP.md §1.3 — AbstractionChecker re-parse.
1404
1413
  for path in py_files:
1405
1414
  try:
1406
1415
  source = path.read_text(encoding="utf-8", errors="replace")
@@ -689,14 +689,22 @@ class SecretChecker(FileChecker):
689
689
  findings.extend(_scan_k8s_secrets(path, source))
690
690
 
691
691
  # SECRET-012: heredoc secrets (skip lines where SECRET-001..010 already fired)
692
- already = {f.line for f in findings if f.rule_id != "PROOFCTL-SECRET-011"}
692
+ already = {
693
+ f.line
694
+ for f in findings
695
+ if f.rule_id != "PROOFCTL-SECRET-011" and f.line is not None
696
+ }
693
697
  findings.extend(_scan_heredoc_secrets(path, source, already))
694
698
 
695
699
  # SECRET-007/013: Python AST-aware detection (kwargs, tuple-auth,
696
700
  # module-level constants, connection strings). Skip lines that already
697
701
  # fired a SECRET-* via line-scan to avoid double-reporting.
698
702
  if path.suffix == ".py":
699
- already_lines = {f.line for f in findings if f.rule_id != "PROOFCTL-SECRET-011"}
703
+ already_lines = {
704
+ f.line
705
+ for f in findings
706
+ if f.rule_id != "PROOFCTL-SECRET-011" and f.line is not None
707
+ }
700
708
  findings.extend(_scan_python_ast(path, source, already_lines))
701
709
 
702
710
  return findings