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.
- {proofctl-0.5.6 → proofctl-0.6.0}/MANIFEST.in +0 -2
- {proofctl-0.5.6/proofctl.egg-info → proofctl-0.6.0}/PKG-INFO +34 -27
- {proofctl-0.5.6 → proofctl-0.6.0}/README.md +23 -23
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/__init__.py +1 -1
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/baseline.py +1 -1
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/dockerfile.py +5 -3
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/hcl_utils.py +1 -1
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/imports.py +0 -110
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/leakage.py +4 -4
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/quality.py +19 -10
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/secrets.py +10 -2
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/security.py +24 -19
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/terraform.py +0 -1
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/variants.py +2 -2
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/yaml_checker.py +21 -25
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/cli.py +13 -9
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/config.py +3 -3
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/engine.py +17 -19
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/fixer.py +11 -13
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/registry.py +6 -6
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/terminal.py +2 -2
- {proofctl-0.5.6 → proofctl-0.6.0/proofctl.egg-info}/PKG-INFO +34 -27
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/requires.txt +2 -0
- proofctl-0.6.0/pyproject.toml +72 -0
- proofctl-0.5.6/pyproject.toml +0 -48
- {proofctl-0.5.6 → proofctl-0.6.0}/LICENSE +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/_globmatch.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/__init__.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/base.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/llm_integration.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/placeholders.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/shell_checker.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/checkers/terragrunt.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/models.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/__init__.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/html_reporter.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl/reporters/json_reporter.py +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/SOURCES.txt +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/dependency_links.txt +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/entry_points.txt +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/proofctl.egg-info/top_level.txt +0 -0
- {proofctl-0.5.6 → proofctl-0.6.0}/setup.cfg +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofctl
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
5
|
-
Author
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: DevOps linter and security scanner for AI slop and misconfigurations — runs in pre-commit, CI, or post-commit; no external tools required
|
|
5
|
+
Author: Kolawolu Odunola
|
|
6
6
|
License: MIT
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
PROOFCTL-
|
|
40
|
-
PROOFCTL-
|
|
41
|
-
PROOFCTL-
|
|
42
|
-
PROOFCTL-
|
|
43
|
-
PROOFCTL-YAML-
|
|
44
|
-
PROOFCTL-
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 |
|
|
176
|
-
| Terraform — General + AI (`TF-T`, `TF-AI`) |
|
|
177
|
-
| Terraform — AWS (`TF-A`) |
|
|
178
|
-
| Terraform — GCP (`TF-G`) |
|
|
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`) |
|
|
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`) |
|
|
184
|
-
| GitHub Actions (`YAML-GHA`) |
|
|
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,
|
|
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
|
|
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
|
-
|
|
11
|
-
PROOFCTL-
|
|
12
|
-
PROOFCTL-
|
|
13
|
-
PROOFCTL-
|
|
14
|
-
PROOFCTL-
|
|
15
|
-
PROOFCTL-YAML-
|
|
16
|
-
PROOFCTL-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 |
|
|
148
|
-
| Terraform — General + AI (`TF-T`, `TF-AI`) |
|
|
149
|
-
| Terraform — AWS (`TF-A`) |
|
|
150
|
-
| Terraform — GCP (`TF-G`) |
|
|
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`) |
|
|
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`) |
|
|
156
|
-
| GitHub Actions (`YAML-GHA`) |
|
|
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,
|
|
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 —
|
|
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
|
|
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-
|
|
366
|
-
docs_url="https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-
|
|
365
|
+
authority="OWASP CICD-SEC-9 – Improper 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
|
-
|
|
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 `~`.
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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="
|
|
160
|
+
authority="CWE-703 – Improper 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
|
-
#
|
|
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
|
-
#
|
|
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,
|
|
1127
|
-
if isinstance(
|
|
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
|
-
#
|
|
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 = {
|
|
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 = {
|
|
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
|