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.
- {proofctl-0.5.7 → proofctl-0.6.0}/MANIFEST.in +0 -2
- {proofctl-0.5.7/proofctl.egg-info → proofctl-0.6.0}/PKG-INFO +34 -27
- {proofctl-0.5.7 → proofctl-0.6.0}/README.md +23 -23
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/__init__.py +1 -1
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/baseline.py +1 -1
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/dockerfile.py +5 -3
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/hcl_utils.py +1 -1
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/imports.py +0 -2
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/leakage.py +4 -4
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/quality.py +19 -10
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/secrets.py +10 -2
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/security.py +24 -19
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/terraform.py +0 -1
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/variants.py +2 -2
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/yaml_checker.py +21 -25
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/cli.py +13 -9
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/config.py +3 -3
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/engine.py +16 -16
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/fixer.py +11 -13
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/registry.py +6 -5
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/terminal.py +2 -2
- {proofctl-0.5.7 → proofctl-0.6.0/proofctl.egg-info}/PKG-INFO +34 -27
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/requires.txt +2 -0
- proofctl-0.6.0/pyproject.toml +72 -0
- proofctl-0.5.7/pyproject.toml +0 -48
- {proofctl-0.5.7 → proofctl-0.6.0}/LICENSE +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/_globmatch.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/__init__.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/base.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/llm_integration.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/placeholders.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/shell_checker.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/checkers/terragrunt.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/models.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/__init__.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/html_reporter.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl/reporters/json_reporter.py +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/SOURCES.txt +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/dependency_links.txt +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/entry_points.txt +0 -0
- {proofctl-0.5.7 → proofctl-0.6.0}/proofctl.egg-info/top_level.txt +0 -0
- {proofctl-0.5.7 → 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
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Security checker — rules PROOFCTL-S-*
|
|
2
2
|
|
|
3
|
-
AI
|
|
4
|
-
|
|
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
|
-
|
|
444
|
-
if isinstance(
|
|
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: {
|
|
450
|
+
rule_name=f"Weak hash algorithm: {str(algo_node.value)}",
|
|
451
451
|
severity=Severity.ERROR,
|
|
452
|
-
message=f'hashlib.new("{
|
|
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
|
-
#
|
|
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
|
-
|
|
2641
|
-
if self._has_xpath_predicate(
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|