proofctl 0.5.7__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.7 → proofctl-0.6.0}/MANIFEST.in +0 -2
  2. {proofctl-0.5.7/proofctl.egg-info → proofctl-0.6.0}/PKG-INFO +34 -27
  3. {proofctl-0.5.7 → proofctl-0.6.0}/README.md +23 -23
  4. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/__init__.py +1 -1
  5. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/baseline.py +1 -1
  6. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/dockerfile.py +5 -3
  7. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/hcl_utils.py +1 -1
  8. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/imports.py +0 -2
  9. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/leakage.py +4 -4
  10. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/quality.py +19 -10
  11. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/secrets.py +10 -2
  12. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/security.py +24 -19
  13. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/terraform.py +0 -1
  14. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/variants.py +2 -2
  15. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/yaml_checker.py +21 -25
  16. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/cli.py +13 -9
  17. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/config.py +3 -3
  18. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/engine.py +16 -16
  19. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/fixer.py +11 -13
  20. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/registry.py +6 -5
  21. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/terminal.py +2 -2
  22. {proofctl-0.5.7 → proofctl-0.6.0/proofctl.egg-info}/PKG-INFO +34 -27
  23. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/requires.txt +2 -0
  24. proofctl-0.6.0/pyproject.toml +72 -0
  25. proofctl-0.5.7/pyproject.toml +0 -48
  26. {proofctl-0.5.7 → proofctl-0.6.0}/LICENSE +0 -0
  27. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/_globmatch.py +0 -0
  28. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/__init__.py +0 -0
  29. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/base.py +0 -0
  30. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/llm_integration.py +0 -0
  31. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/placeholders.py +0 -0
  32. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/shell_checker.py +0 -0
  33. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/terragrunt.py +0 -0
  34. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/models.py +0 -0
  35. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/__init__.py +0 -0
  36. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/html_reporter.py +0 -0
  37. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/json_reporter.py +0 -0
  38. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/SOURCES.txt +0 -0
  39. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/dependency_links.txt +0 -0
  40. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/entry_points.txt +0 -0
  41. {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/top_level.txt +0 -0
  42. {proofctl-0.5.7 → 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.7
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. **268 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 | 11 | Placeholders, leakage (cross-language idioms, PII), import hallucination, 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. **268 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 | 11 | Placeholders, leakage (cross-language idioms, PII), import hallucination, 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
@@ -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
@@ -1,7 +1,7 @@
1
1
  """Security checker — rules PROOFCTL-S-*
2
2
 
3
- AI models introduce these vulnerabilities at high rates (Veracode 2024: 2.7× higher
4
- density than human-written code). All rules are AST-based for precision.
3
+ AI-generated code introduces these vulnerability classes at elevated rates relative
4
+ to human-written code. All rules are AST-based for precision.
5
5
 
6
6
  S-001 SQL injection — string interpolation/concatenation in .execute() calls
7
7
  S-002 Command injection — subprocess/os.system with shell=True + variable arg
@@ -440,16 +440,16 @@ class _S004Visitor(ast.NodeVisitor):
440
440
  docs_url="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf",
441
441
  ))
442
442
  elif chain[1] == "new" and node.args:
443
- algo = node.args[0]
444
- if isinstance(algo, ast.Constant) and str(algo.value).lower() in ("md5", "sha1", "sha-1"):
443
+ algo_node = node.args[0]
444
+ if isinstance(algo_node, ast.Constant) and str(algo_node.value).lower() in ("md5", "sha1", "sha-1"):
445
445
  self.findings.append(Finding(
446
446
  file=str(self.path),
447
447
  line=node.lineno,
448
448
  col=node.col_offset,
449
449
  rule_id="PROOFCTL-S-004",
450
- rule_name=f"Weak hash algorithm: {algo.value}",
450
+ rule_name=f"Weak hash algorithm: {str(algo_node.value)}",
451
451
  severity=Severity.ERROR,
452
- message=f'hashlib.new("{algo.value}") uses a weak algorithm',
452
+ message=f'hashlib.new("{str(algo_node.value)}") uses a weak algorithm',
453
453
  hint="Use hashlib.new('sha256') or stronger.",
454
454
  authority="CWE-327",
455
455
  docs_url="https://cwe.mitre.org/data/definitions/327.html",
@@ -527,7 +527,7 @@ class _S004Visitor(ast.NodeVisitor):
527
527
 
528
528
  def visit_Assign(self, node: ast.Assign) -> None:
529
529
  self.generic_visit(node)
530
- # H-12: handle both ast.Name and ast.Attribute targets.
530
+ # Handle both ast.Name and ast.Attribute targets.
531
531
  for t in node.targets:
532
532
  target_name = _assign_target_name(t)
533
533
  if target_name:
@@ -1232,8 +1232,8 @@ class _S010Visitor(ast.NodeVisitor):
1232
1232
  def _flag_info_exposure(self, node: ast.AST, detail: str) -> None:
1233
1233
  self.findings.append(Finding(
1234
1234
  file=str(self.path),
1235
- line=node.lineno,
1236
- col=node.col_offset,
1235
+ line=node.lineno, # type: ignore[attr-defined] # concrete AST node passed in always has position info
1236
+ col=node.col_offset, # type: ignore[attr-defined] # concrete AST node passed in always has position info
1237
1237
  rule_id="PROOFCTL-S-010",
1238
1238
  rule_name="Information exposure via exception/error",
1239
1239
  severity=Severity.WARNING,
@@ -1854,6 +1854,8 @@ class _S015Visitor(ast.NodeVisitor):
1854
1854
  if not self._SENSITIVE_RE.search(target_name):
1855
1855
  return
1856
1856
  for node in ast.walk(value):
1857
+ if not isinstance(node, ast.Call):
1858
+ continue
1857
1859
  fn = self._is_random_call(node)
1858
1860
  if fn:
1859
1861
  self._flag(node, fn, target_name)
@@ -1877,11 +1879,13 @@ class _S015Visitor(ast.NodeVisitor):
1877
1879
  for sub in ast.walk(node):
1878
1880
  if isinstance(sub, ast.Return) and sub.value is not None:
1879
1881
  fn = self._is_random_call(sub.value)
1880
- if fn:
1882
+ if fn and isinstance(sub.value, ast.Call):
1881
1883
  self._flag(sub.value, fn, node.name)
1882
1884
  else:
1883
1885
  # Walk into expressions like ''.join(random.choice(..) for _)
1884
1886
  for inner in ast.walk(sub.value):
1887
+ if not isinstance(inner, ast.Call):
1888
+ continue
1885
1889
  fn = self._is_random_call(inner)
1886
1890
  if fn:
1887
1891
  self._flag(inner, fn, node.name)
@@ -2147,8 +2151,8 @@ class _S017Visitor(_BaseTaintVisitor):
2147
2151
  def _flag(self, node: ast.AST, what: str) -> None:
2148
2152
  self.findings.append(Finding(
2149
2153
  file=str(self.path),
2150
- line=node.lineno,
2151
- col=node.col_offset,
2154
+ line=node.lineno, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2155
+ col=node.col_offset, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2152
2156
  rule_id="PROOFCTL-S-017",
2153
2157
  rule_name="Cross-site scripting (XSS)",
2154
2158
  severity=Severity.ERROR,
@@ -2396,8 +2400,8 @@ class _S019Visitor(ast.NodeVisitor):
2396
2400
  def _flag(self, node: ast.AST, what: str) -> None:
2397
2401
  self.findings.append(Finding(
2398
2402
  file=str(self.path),
2399
- line=node.lineno,
2400
- col=node.col_offset,
2403
+ line=node.lineno, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2404
+ col=node.col_offset, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2401
2405
  rule_id="PROOFCTL-S-019",
2402
2406
  rule_name="Unrestricted file upload",
2403
2407
  severity=Severity.WARNING,
@@ -2522,8 +2526,8 @@ class _S020Visitor(_BaseTaintVisitor):
2522
2526
  def _flag(self, node: ast.AST, message: str, hint: str) -> None:
2523
2527
  self.findings.append(Finding(
2524
2528
  file=str(self.path),
2525
- line=node.lineno,
2526
- col=node.col_offset,
2529
+ line=node.lineno, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2530
+ col=node.col_offset, # type: ignore[attr-defined] # concrete AST node passed in always has position info
2527
2531
  rule_id="PROOFCTL-S-020",
2528
2532
  rule_name="Insufficiently protected credentials",
2529
2533
  severity=Severity.ERROR,
@@ -2637,8 +2641,8 @@ class _S022Visitor(_BaseTaintVisitor):
2637
2641
  ):
2638
2642
  # Only flag if the query is interpolated AND uses XPath-like syntax with a predicate
2639
2643
  if isinstance(sub.args[0], (ast.JoinedStr, ast.BinOp)):
2640
- q = sub.args[0]
2641
- if self._has_xpath_predicate(q) and self._is_interpolated_with_taint(q):
2644
+ fmt_query = sub.args[0]
2645
+ if self._has_xpath_predicate(fmt_query) and self._is_interpolated_with_taint(fmt_query):
2642
2646
  target = sub
2643
2647
 
2644
2648
  if target is None or not target.args:
@@ -2731,7 +2735,8 @@ class _S022Visitor(_BaseTaintVisitor):
2731
2735
  class _S023Visitor(ast.NodeVisitor):
2732
2736
  """CSRF — Django @csrf_exempt on a POST handler.
2733
2737
 
2734
- Note: cross-file Flask CSRF detection is out of scope for this rule. TODO.
2738
+ Note: this rule is intra-file only; cross-file Flask CSRF detection is
2739
+ intentionally out of scope (it would require whole-project taint tracking).
2735
2740
  """
2736
2741
 
2737
2742
  def __init__(self, path: Path):
@@ -2674,7 +2674,6 @@ class TerraformChecker(HclFileChecker):
2674
2674
  def _a024(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
2675
2675
  """A-024 — Security group ingress on sensitive ports from 0.0.0.0/0."""
2676
2676
  out = []
2677
- port_re = re.compile(r'(?:from_port|to_port)\s*=\s*(\d+)')
2678
2677
  for b in blocks:
2679
2678
  if b.kind != "resource":
2680
2679
  continue
@@ -38,7 +38,7 @@ def _token_multiset(source: str) -> collections.Counter:
38
38
 
39
39
 
40
40
  def _bucket_keys(counter: collections.Counter, threshold: float) -> list[str]:
41
- """Hash-bucket keys for cheap near-duplicate pre-filtering (H-22).
41
+ """Hash-bucket keys for cheap near-duplicate pre-filtering.
42
42
 
43
43
  Two files with Jaccard similarity >= `threshold` must share at least one
44
44
  of these bucket keys; files that share none cannot meet the threshold,
@@ -167,7 +167,7 @@ class VariantChecker(DirectoryChecker):
167
167
  except OSError:
168
168
  pass
169
169
 
170
- # H-22: hash-bucket pre-pass replaces the old 20-file cap.
170
+ # Hash-bucket pre-pass replaces an earlier 20-file cap.
171
171
  # Build buckets keyed by (rounded-size-band, top-token-hash).
172
172
  # Only files that share a bucket are eligible for the expensive
173
173
  # Jaccard comparison.