proofctl 0.4.1__tar.gz → 0.4.3__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.4.1 → proofctl-0.4.3}/LICENSE +1 -1
- proofctl-0.4.3/MANIFEST.in +19 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/PKG-INFO +5 -5
- {proofctl-0.4.1 → proofctl-0.4.3}/README.md +4 -4
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/terraform.py +179 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/PKG-INFO +5 -5
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/SOURCES.txt +2 -33
- {proofctl-0.4.1 → proofctl-0.4.3}/pyproject.toml +1 -1
- proofctl-0.4.1/tests/test_baseline.py +0 -133
- proofctl-0.4.1/tests/test_config_proofctl.py +0 -133
- proofctl-0.4.1/tests/test_dockerfile.py +0 -473
- proofctl-0.4.1/tests/test_engine.py +0 -191
- proofctl-0.4.1/tests/test_fixer.py +0 -163
- proofctl-0.4.1/tests/test_imports.py +0 -312
- proofctl-0.4.1/tests/test_leakage.py +0 -179
- proofctl-0.4.1/tests/test_llm.py +0 -279
- proofctl-0.4.1/tests/test_local_packages.py +0 -239
- proofctl-0.4.1/tests/test_phase1_audit_fixes.py +0 -176
- proofctl-0.4.1/tests/test_phase2_audit_fixes.py +0 -311
- proofctl-0.4.1/tests/test_phase3_audit_fixes.py +0 -305
- proofctl-0.4.1/tests/test_phase4_audit_fixes.py +0 -416
- proofctl-0.4.1/tests/test_phase5_llm_imports_fixes.py +0 -306
- proofctl-0.4.1/tests/test_phase5_p_l_v_fixes.py +0 -279
- proofctl-0.4.1/tests/test_phase5_quality_fixes.py +0 -240
- proofctl-0.4.1/tests/test_phase6_audit_fixes.py +0 -359
- proofctl-0.4.1/tests/test_phase8_audit_fixes.py +0 -328
- proofctl-0.4.1/tests/test_placeholders.py +0 -239
- proofctl-0.4.1/tests/test_quality.py +0 -595
- proofctl-0.4.1/tests/test_quality_ext.py +0 -286
- proofctl-0.4.1/tests/test_secrets.py +0 -512
- proofctl-0.4.1/tests/test_security.py +0 -509
- proofctl-0.4.1/tests/test_security_ext.py +0 -476
- proofctl-0.4.1/tests/test_security_external.py +0 -546
- proofctl-0.4.1/tests/test_suppression.py +0 -137
- proofctl-0.4.1/tests/test_terraform.py +0 -861
- proofctl-0.4.1/tests/test_terraform_ext.py +0 -2806
- proofctl-0.4.1/tests/test_terragrunt.py +0 -313
- proofctl-0.4.1/tests/test_variants.py +0 -136
- proofctl-0.4.1/tests/test_yaml.py +0 -469
- proofctl-0.4.1/tests/test_yaml_k8s_gha.py +0 -1387
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/__init__.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/_globmatch.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/baseline.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/__init__.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/base.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/dockerfile.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/hcl_utils.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/imports.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/leakage.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/llm_integration.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/placeholders.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/quality.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/secrets.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/security.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/shell_checker.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/terragrunt.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/variants.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/yaml_checker.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/cli.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/config.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/engine.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/fixer.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/models.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/registry.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/__init__.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/html_reporter.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/json_reporter.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/terminal.py +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/dependency_links.txt +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/entry_points.txt +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/requires.txt +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/top_level.txt +0 -0
- {proofctl-0.4.1 → proofctl-0.4.3}/setup.cfg +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
graft proofctl
|
|
2
|
+
include README.md
|
|
3
|
+
include LICENSE
|
|
4
|
+
include pyproject.toml
|
|
5
|
+
|
|
6
|
+
# Internal-only — never ship to PyPI
|
|
7
|
+
prune tests
|
|
8
|
+
prune doc
|
|
9
|
+
prune docs
|
|
10
|
+
prune examples
|
|
11
|
+
prune build
|
|
12
|
+
prune dist
|
|
13
|
+
prune .github
|
|
14
|
+
exclude AUDIT_AND_ROADMAP.md
|
|
15
|
+
exclude AUDIT_VERIFICATION_v0.1.11.md
|
|
16
|
+
|
|
17
|
+
global-exclude __pycache__
|
|
18
|
+
global-exclude *.py[cod]
|
|
19
|
+
global-exclude .DS_Store
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofctl
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit
|
|
5
5
|
Author-email: Kolawolu Odunola <kolawolu.o@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
# proofctl
|
|
30
30
|
|
|
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. **
|
|
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. **267 rules**, zero dependencies, runs in under a second.
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
proofctl check .
|
|
@@ -165,7 +165,7 @@ git commit -m "chore: add proofctl baseline"
|
|
|
165
165
|
|
|
166
166
|
## Rules
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
267 rules across 14 families. Run `proofctl rules` after install to enumerate.
|
|
169
169
|
|
|
170
170
|
| Family | Count | What it covers |
|
|
171
171
|
|---|---|---|
|
|
@@ -173,9 +173,9 @@ git commit -m "chore: add proofctl baseline"
|
|
|
173
173
|
| 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
174
|
| Secrets (`SECRET`) | 13 | AWS / GCP / GitHub / Slack / Stripe / private keys / JWTs / hardcoded passwords / K8s Secret YAML / connection strings with embedded creds |
|
|
175
175
|
| Python — Other | 12 | Placeholders, leakage (cross-language idioms, PII), import hallucination, method complexity, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
176
|
-
| Terraform — General + AI (`TF-T`, `TF-AI`) |
|
|
176
|
+
| Terraform — General + AI (`TF-T`, `TF-AI`) | 28 | 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 |
|
|
177
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`) |
|
|
178
|
+
| Terraform — GCP (`TF-G`) | 51 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress, compute access scopes |
|
|
179
179
|
| Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
|
|
180
180
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 12 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
181
181
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
@@ -1,6 +1,6 @@
|
|
|
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. **
|
|
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. **267 rules**, zero dependencies, runs in under a second.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
proofctl check .
|
|
@@ -137,7 +137,7 @@ git commit -m "chore: add proofctl baseline"
|
|
|
137
137
|
|
|
138
138
|
## Rules
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
267 rules across 14 families. Run `proofctl rules` after install to enumerate.
|
|
141
141
|
|
|
142
142
|
| Family | Count | What it covers |
|
|
143
143
|
|---|---|---|
|
|
@@ -145,9 +145,9 @@ git commit -m "chore: add proofctl baseline"
|
|
|
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
147
|
| Python — Other | 12 | Placeholders, leakage (cross-language idioms, PII), import hallucination, method complexity, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
148
|
-
| Terraform — General + AI (`TF-T`, `TF-AI`) |
|
|
148
|
+
| Terraform — General + AI (`TF-T`, `TF-AI`) | 28 | 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 |
|
|
149
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`) |
|
|
150
|
+
| Terraform — GCP (`TF-G`) | 51 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress, compute access scopes |
|
|
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
152
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 12 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
153
153
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
@@ -294,6 +294,7 @@ class TerraformChecker(HclFileChecker):
|
|
|
294
294
|
out += self._g044(path, blocks)
|
|
295
295
|
out += self._g045(path, blocks)
|
|
296
296
|
out += self._g046(path, blocks)
|
|
297
|
+
out += self._g047(path, blocks)
|
|
297
298
|
|
|
298
299
|
# Group AZ — Azure
|
|
299
300
|
out += self._az001(path, blocks)
|
|
@@ -327,6 +328,8 @@ class TerraformChecker(HclFileChecker):
|
|
|
327
328
|
out += self._ai009(path, lines)
|
|
328
329
|
out += self._ai010(path, lines)
|
|
329
330
|
out += self._ai011(path, lines)
|
|
331
|
+
out += self._ai012(path, blocks)
|
|
332
|
+
out += self._ai013(path, blocks)
|
|
330
333
|
|
|
331
334
|
return out
|
|
332
335
|
|
|
@@ -2935,6 +2938,36 @@ class TerraformChecker(HclFileChecker):
|
|
|
2935
2938
|
break
|
|
2936
2939
|
return out
|
|
2937
2940
|
|
|
2941
|
+
def _g047(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
2942
|
+
"""G-047 — Compute instance with `cloud-platform` access scope.
|
|
2943
|
+
|
|
2944
|
+
Access scopes are a coarse, IAM-bypassing layer on top of service-account
|
|
2945
|
+
permissions. `cloud-platform` grants the metadata server permission to
|
|
2946
|
+
mint tokens for every GCP API — so any role the SA ever acquires becomes
|
|
2947
|
+
immediately usable from the VM. Best practice: rely on IAM only, and
|
|
2948
|
+
narrow the scope to `cloud-platform.read-only` or use the default
|
|
2949
|
+
scopes set with explicit IAM roles.
|
|
2950
|
+
"""
|
|
2951
|
+
out = []
|
|
2952
|
+
for b in blocks:
|
|
2953
|
+
if b.kind != "resource" or b.label1 != "google_compute_instance":
|
|
2954
|
+
continue
|
|
2955
|
+
for sa in b.nested("service_account"):
|
|
2956
|
+
# Match "cloud-platform" exactly inside the scopes list — not
|
|
2957
|
+
# cloud-platform.read-only or other narrower scopes.
|
|
2958
|
+
if sa.contains(r'"cloud-platform"'):
|
|
2959
|
+
out.append(self._f(
|
|
2960
|
+
path, sa.attr_line("scopes") or sa.start_line,
|
|
2961
|
+
"PROOFCTL-TF-G047", "Compute instance with cloud-platform scope",
|
|
2962
|
+
Severity.WARNING,
|
|
2963
|
+
f'google_compute_instance "{b.label2}" has access scope "cloud-platform" — broadest possible API access',
|
|
2964
|
+
hint='Use narrower scopes (e.g. "cloud-platform.read-only") or omit and rely on IAM bindings on the service account.',
|
|
2965
|
+
authority="GCP Compute Engine — Access scopes vs IAM",
|
|
2966
|
+
docs_url="https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice",
|
|
2967
|
+
))
|
|
2968
|
+
break
|
|
2969
|
+
return out
|
|
2970
|
+
|
|
2938
2971
|
# ════════════════════════════════════════════════════════════════════════
|
|
2939
2972
|
# Group AZ — Azure data-exfiltration rules (extended)
|
|
2940
2973
|
# ════════════════════════════════════════════════════════════════════════
|
|
@@ -3328,6 +3361,152 @@ class TerraformChecker(HclFileChecker):
|
|
|
3328
3361
|
))
|
|
3329
3362
|
return out
|
|
3330
3363
|
|
|
3364
|
+
# GCE boot disk that references an image *family* rather than a specific
|
|
3365
|
+
# image is the GCP equivalent of `:latest` — Google promotes new images
|
|
3366
|
+
# into a family on its own schedule. Any apply that triggers VM rebuild
|
|
3367
|
+
# picks up whatever's current that day. Sneaky drift / surprise OS upgrade
|
|
3368
|
+
# vector that AI tools default to because the canonical examples use it.
|
|
3369
|
+
_IMAGE_FAMILY_RE = re.compile(
|
|
3370
|
+
r'image\s*=\s*"[^"]*?/images/family/[^"]+"',
|
|
3371
|
+
)
|
|
3372
|
+
|
|
3373
|
+
def _ai012(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
3374
|
+
"""AI-012 — GCE boot disk references an image *family* (mutable)."""
|
|
3375
|
+
out = []
|
|
3376
|
+
for b in blocks:
|
|
3377
|
+
if b.kind != "resource" or b.label1 != "google_compute_instance":
|
|
3378
|
+
continue
|
|
3379
|
+
for bd in b.nested("boot_disk"):
|
|
3380
|
+
# `image` can live either directly on boot_disk or inside
|
|
3381
|
+
# the nested initialize_params block.
|
|
3382
|
+
targets = [bd] + bd.nested("initialize_params")
|
|
3383
|
+
for t in targets:
|
|
3384
|
+
line = t.any_line_matches(self._IMAGE_FAMILY_RE.pattern)
|
|
3385
|
+
if line is not None:
|
|
3386
|
+
out.append(self._f(
|
|
3387
|
+
path, line,
|
|
3388
|
+
"PROOFCTL-TF-AI-012",
|
|
3389
|
+
"GCE boot disk uses image family (mutable)",
|
|
3390
|
+
Severity.WARNING,
|
|
3391
|
+
f'google_compute_instance "{b.label2}" references an image family — Google promotes new images into the family at any time',
|
|
3392
|
+
hint="Pin to a specific image name (e.g. ubuntu-2404-amd64-20251101) or set lifecycle { ignore_changes = [boot_disk[0].initialize_params[0].image] } to opt out of rebuilds.",
|
|
3393
|
+
authority="GCP Compute Engine — Image families",
|
|
3394
|
+
docs_url="https://cloud.google.com/compute/docs/images/image-families-best-practices",
|
|
3395
|
+
))
|
|
3396
|
+
break # one finding per boot_disk is enough
|
|
3397
|
+
return out
|
|
3398
|
+
|
|
3399
|
+
# Variables likely to hold credential material — used by AI-013 to decide
|
|
3400
|
+
# which `var.X` references are dangerous to embed in compute metadata.
|
|
3401
|
+
# Aligns with the SECRET-007 name set so engineers don't have to learn
|
|
3402
|
+
# two parallel taxonomies.
|
|
3403
|
+
_SENSITIVE_VAR_NAME_RE = re.compile(
|
|
3404
|
+
r"\bvar\.((?:[a-zA-Z0-9_]*_)?"
|
|
3405
|
+
r"(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|"
|
|
3406
|
+
r"private[_-]?key|client[_-]?secret|encryption[_-]?key|auth[_-]?token|"
|
|
3407
|
+
r"bearer[_-]?token|jwt|credential|tunnel[_-]?token)"
|
|
3408
|
+
r"[a-zA-Z0-9_]*)\b",
|
|
3409
|
+
re.IGNORECASE,
|
|
3410
|
+
)
|
|
3411
|
+
|
|
3412
|
+
@staticmethod
|
|
3413
|
+
def _line_range_of_attr(
|
|
3414
|
+
block: HclBlock, attr_name: str
|
|
3415
|
+
) -> tuple[int, int] | None:
|
|
3416
|
+
"""Find (start_idx, end_idx) into block.raw_lines that span the attribute
|
|
3417
|
+
``attr_name = <value>`` including any continuation lines opened by
|
|
3418
|
+
parentheses or brackets.
|
|
3419
|
+
|
|
3420
|
+
Returns indices relative to ``block.raw_lines`` (0-based), inclusive of
|
|
3421
|
+
the closing bracket / paren line. Returns None if the attribute is not
|
|
3422
|
+
present.
|
|
3423
|
+
"""
|
|
3424
|
+
head = re.compile(rf"^\s*{re.escape(attr_name)}\s*=\s*(.*)$")
|
|
3425
|
+
for i, line in enumerate(block.raw_lines):
|
|
3426
|
+
m = head.match(line)
|
|
3427
|
+
if not m:
|
|
3428
|
+
continue
|
|
3429
|
+
rest = m.group(1)
|
|
3430
|
+
depth = rest.count("(") + rest.count("[") + rest.count("{")
|
|
3431
|
+
depth -= rest.count(")") + rest.count("]") + rest.count("}")
|
|
3432
|
+
if depth <= 0:
|
|
3433
|
+
return (i, i)
|
|
3434
|
+
j = i + 1
|
|
3435
|
+
while j < len(block.raw_lines) and depth > 0:
|
|
3436
|
+
ln = block.raw_lines[j]
|
|
3437
|
+
depth += ln.count("(") + ln.count("[") + ln.count("{")
|
|
3438
|
+
depth -= ln.count(")") + ln.count("]") + ln.count("}")
|
|
3439
|
+
j += 1
|
|
3440
|
+
return (i, j - 1)
|
|
3441
|
+
return None
|
|
3442
|
+
|
|
3443
|
+
def _ai013(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
3444
|
+
"""AI-013 — Sensitive variable interpolated into compute metadata.
|
|
3445
|
+
|
|
3446
|
+
Pattern: `var.<secret-shaped-name>` appears inside ``metadata_startup_script``
|
|
3447
|
+
or any nested ``metadata { ... }`` block on a ``google_compute_instance``
|
|
3448
|
+
resource.
|
|
3449
|
+
|
|
3450
|
+
Risk: anything that ends up in VM metadata is readable by any caller
|
|
3451
|
+
with ``compute.instances.get`` and is written to plan / state. The
|
|
3452
|
+
Cloudflare-tunnel-in-startup-script pattern is the canonical AI-slop
|
|
3453
|
+
instance.
|
|
3454
|
+
"""
|
|
3455
|
+
out = []
|
|
3456
|
+
seen: set[tuple[Path, int]] = set()
|
|
3457
|
+
|
|
3458
|
+
def _emit(b: HclBlock, line: int, var_name: str, surface: str) -> None:
|
|
3459
|
+
key = (path, line)
|
|
3460
|
+
if key in seen:
|
|
3461
|
+
return
|
|
3462
|
+
seen.add(key)
|
|
3463
|
+
out.append(self._f(
|
|
3464
|
+
path, line,
|
|
3465
|
+
"PROOFCTL-TF-AI-013",
|
|
3466
|
+
"Sensitive variable in compute metadata",
|
|
3467
|
+
Severity.ERROR,
|
|
3468
|
+
f'google_compute_instance "{b.label2}" passes var.{var_name} into {surface} — secret will be readable from VM metadata and visible in plan/state',
|
|
3469
|
+
hint=(
|
|
3470
|
+
"Mount the secret from Secret Manager inside the VM at runtime "
|
|
3471
|
+
"(google_secret_manager_secret_iam_member + a startup script that "
|
|
3472
|
+
"calls `gcloud secrets versions access`). Do not bake the value into metadata."
|
|
3473
|
+
),
|
|
3474
|
+
authority="CIS GCP Benchmark 4.6 / OWASP A02 Cryptographic Failures",
|
|
3475
|
+
docs_url="https://cloud.google.com/compute/docs/metadata/overview#metadata_security_considerations",
|
|
3476
|
+
))
|
|
3477
|
+
|
|
3478
|
+
for b in blocks:
|
|
3479
|
+
if b.kind != "resource" or b.label1 != "google_compute_instance":
|
|
3480
|
+
continue
|
|
3481
|
+
# 1. Scan metadata_startup_script line range.
|
|
3482
|
+
rng = self._line_range_of_attr(b, "metadata_startup_script")
|
|
3483
|
+
if rng is not None:
|
|
3484
|
+
lo, hi = rng
|
|
3485
|
+
for offset in range(lo, hi + 1):
|
|
3486
|
+
line_text = b.raw_lines[offset]
|
|
3487
|
+
m = self._SENSITIVE_VAR_NAME_RE.search(line_text)
|
|
3488
|
+
if m:
|
|
3489
|
+
_emit(b, b.start_line + offset, m.group(1),
|
|
3490
|
+
"metadata_startup_script")
|
|
3491
|
+
# 2. Scan nested metadata { ... } blocks (HCL block form).
|
|
3492
|
+
for md in b.nested("metadata"):
|
|
3493
|
+
for offset, line_text in enumerate(md.raw_lines):
|
|
3494
|
+
m = self._SENSITIVE_VAR_NAME_RE.search(line_text)
|
|
3495
|
+
if m:
|
|
3496
|
+
_emit(b, md.start_line + offset, m.group(1),
|
|
3497
|
+
"metadata block")
|
|
3498
|
+
# 3. Scan metadata = { ... } attribute (HCL object-attribute form).
|
|
3499
|
+
rng = self._line_range_of_attr(b, "metadata")
|
|
3500
|
+
if rng is not None:
|
|
3501
|
+
lo, hi = rng
|
|
3502
|
+
for offset in range(lo, hi + 1):
|
|
3503
|
+
line_text = b.raw_lines[offset]
|
|
3504
|
+
m = self._SENSITIVE_VAR_NAME_RE.search(line_text)
|
|
3505
|
+
if m:
|
|
3506
|
+
_emit(b, b.start_line + offset, m.group(1),
|
|
3507
|
+
"metadata block")
|
|
3508
|
+
return out
|
|
3509
|
+
|
|
3331
3510
|
|
|
3332
3511
|
# ── Directory-level checker (cross-file rules) ───────────────────────────────
|
|
3333
3512
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofctl
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit
|
|
5
5
|
Author-email: Kolawolu Odunola <kolawolu.o@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
# proofctl
|
|
30
30
|
|
|
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. **
|
|
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. **267 rules**, zero dependencies, runs in under a second.
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
proofctl check .
|
|
@@ -165,7 +165,7 @@ git commit -m "chore: add proofctl baseline"
|
|
|
165
165
|
|
|
166
166
|
## Rules
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
267 rules across 14 families. Run `proofctl rules` after install to enumerate.
|
|
169
169
|
|
|
170
170
|
| Family | Count | What it covers |
|
|
171
171
|
|---|---|---|
|
|
@@ -173,9 +173,9 @@ git commit -m "chore: add proofctl baseline"
|
|
|
173
173
|
| 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
174
|
| Secrets (`SECRET`) | 13 | AWS / GCP / GitHub / Slack / Stripe / private keys / JWTs / hardcoded passwords / K8s Secret YAML / connection strings with embedded creds |
|
|
175
175
|
| Python — Other | 12 | Placeholders, leakage (cross-language idioms, PII), import hallucination, method complexity, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
176
|
-
| Terraform — General + AI (`TF-T`, `TF-AI`) |
|
|
176
|
+
| Terraform — General + AI (`TF-T`, `TF-AI`) | 28 | 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 |
|
|
177
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`) |
|
|
178
|
+
| Terraform — GCP (`TF-G`) | 51 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress, compute access scopes |
|
|
179
179
|
| Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
|
|
180
180
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 12 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
181
181
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
4
5
|
proofctl/__init__.py
|
|
@@ -35,36 +36,4 @@ proofctl/checkers/yaml_checker.py
|
|
|
35
36
|
proofctl/reporters/__init__.py
|
|
36
37
|
proofctl/reporters/html_reporter.py
|
|
37
38
|
proofctl/reporters/json_reporter.py
|
|
38
|
-
proofctl/reporters/terminal.py
|
|
39
|
-
tests/test_baseline.py
|
|
40
|
-
tests/test_config_proofctl.py
|
|
41
|
-
tests/test_dockerfile.py
|
|
42
|
-
tests/test_engine.py
|
|
43
|
-
tests/test_fixer.py
|
|
44
|
-
tests/test_imports.py
|
|
45
|
-
tests/test_leakage.py
|
|
46
|
-
tests/test_llm.py
|
|
47
|
-
tests/test_local_packages.py
|
|
48
|
-
tests/test_phase1_audit_fixes.py
|
|
49
|
-
tests/test_phase2_audit_fixes.py
|
|
50
|
-
tests/test_phase3_audit_fixes.py
|
|
51
|
-
tests/test_phase4_audit_fixes.py
|
|
52
|
-
tests/test_phase5_llm_imports_fixes.py
|
|
53
|
-
tests/test_phase5_p_l_v_fixes.py
|
|
54
|
-
tests/test_phase5_quality_fixes.py
|
|
55
|
-
tests/test_phase6_audit_fixes.py
|
|
56
|
-
tests/test_phase8_audit_fixes.py
|
|
57
|
-
tests/test_placeholders.py
|
|
58
|
-
tests/test_quality.py
|
|
59
|
-
tests/test_quality_ext.py
|
|
60
|
-
tests/test_secrets.py
|
|
61
|
-
tests/test_security.py
|
|
62
|
-
tests/test_security_ext.py
|
|
63
|
-
tests/test_security_external.py
|
|
64
|
-
tests/test_suppression.py
|
|
65
|
-
tests/test_terraform.py
|
|
66
|
-
tests/test_terraform_ext.py
|
|
67
|
-
tests/test_terragrunt.py
|
|
68
|
-
tests/test_variants.py
|
|
69
|
-
tests/test_yaml.py
|
|
70
|
-
tests/test_yaml_k8s_gha.py
|
|
39
|
+
proofctl/reporters/terminal.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofctl"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Zero-dependency linter for Python, Terraform, Dockerfiles, Kubernetes, and GitHub Actions — catches AI slop and security misconfigurations pre-commit"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import tempfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from proofctl.baseline import (
|
|
8
|
-
filter_new_findings,
|
|
9
|
-
load_baseline,
|
|
10
|
-
save_baseline,
|
|
11
|
-
)
|
|
12
|
-
from proofctl.models import Finding, Severity
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _f(rule_id: str, file: str = "foo.py", msg: str = "test msg", line: int = 1) -> Finding:
|
|
16
|
-
return Finding(
|
|
17
|
-
file=file, line=line, col=0,
|
|
18
|
-
rule_id=rule_id, rule_name="Test",
|
|
19
|
-
severity=Severity.ERROR, message=msg,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# ── save / load ───────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
def test_save_creates_file():
|
|
26
|
-
with tempfile.TemporaryDirectory() as td:
|
|
27
|
-
root = Path(td)
|
|
28
|
-
findings = [_f("PROOFCTL-P-001")]
|
|
29
|
-
out = save_baseline(findings, root)
|
|
30
|
-
assert out.exists()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_save_load_roundtrip():
|
|
34
|
-
"""H-10: load_baseline returns the full versioned dict; the actual finding
|
|
35
|
-
list lives under `findings`. Schema is bumped to 2 by save_baseline."""
|
|
36
|
-
with tempfile.TemporaryDirectory() as td:
|
|
37
|
-
root = Path(td)
|
|
38
|
-
findings = [_f("PROOFCTL-P-001", msg="'foo' body is pass")]
|
|
39
|
-
save_baseline(findings, root)
|
|
40
|
-
loaded = load_baseline(root)
|
|
41
|
-
assert loaded is not None
|
|
42
|
-
assert loaded["schema_version"] >= 2
|
|
43
|
-
assert len(loaded["findings"]) == 1
|
|
44
|
-
assert loaded["findings"][0]["rule_id"] == "PROOFCTL-P-001"
|
|
45
|
-
assert loaded["findings"][0]["message"] == "'foo' body is pass"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_load_returns_none_when_no_file():
|
|
49
|
-
with tempfile.TemporaryDirectory() as td:
|
|
50
|
-
assert load_baseline(Path(td)) is None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_load_returns_none_on_corrupt_file():
|
|
54
|
-
with tempfile.TemporaryDirectory() as td:
|
|
55
|
-
root = Path(td)
|
|
56
|
-
(root / ".proofctl-baseline.json").write_text("not json")
|
|
57
|
-
assert load_baseline(root) is None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def test_save_count_field():
|
|
61
|
-
with tempfile.TemporaryDirectory() as td:
|
|
62
|
-
root = Path(td)
|
|
63
|
-
save_baseline([_f("PROOFCTL-P-001"), _f("PROOFCTL-L-001")], root)
|
|
64
|
-
data = json.loads((root / ".proofctl-baseline.json").read_text())
|
|
65
|
-
assert data["count"] == 2
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# ── filter_new_findings ───────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
def test_all_in_baseline_suppressed():
|
|
71
|
-
f = _f("PROOFCTL-P-001", msg="'foo' body is pass")
|
|
72
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"}]
|
|
73
|
-
assert filter_new_findings([f], baseline) == []
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_new_finding_not_suppressed():
|
|
77
|
-
f = _f("PROOFCTL-P-001", msg="'bar' body is pass")
|
|
78
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"}]
|
|
79
|
-
assert filter_new_findings([f], baseline) == [f]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_different_rule_not_suppressed():
|
|
83
|
-
f = _f("PROOFCTL-L-001", msg="'foo' body is pass")
|
|
84
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"}]
|
|
85
|
-
assert filter_new_findings([f], baseline) == [f]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_different_file_not_suppressed():
|
|
89
|
-
f = _f("PROOFCTL-P-001", file="bar.py", msg="'foo' body is pass")
|
|
90
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"}]
|
|
91
|
-
assert filter_new_findings([f], baseline) == [f]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_line_shift_suppressed():
|
|
95
|
-
# Same bug moved from line 10 to line 42 — message unchanged → still suppressed.
|
|
96
|
-
f = _f("PROOFCTL-P-001", msg="'foo' body is pass", line=42)
|
|
97
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"}]
|
|
98
|
-
assert filter_new_findings([f], baseline) == []
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_empty_baseline_suppresses_nothing():
|
|
102
|
-
f = _f("PROOFCTL-P-001")
|
|
103
|
-
assert filter_new_findings([f], []) == [f]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_multiset_exact_count_suppressed():
|
|
107
|
-
# Two identical findings in baseline suppress exactly two in new run.
|
|
108
|
-
f1 = _f("PROOFCTL-P-001", msg="'foo' body is pass")
|
|
109
|
-
f2 = _f("PROOFCTL-P-001", msg="'foo' body is pass")
|
|
110
|
-
baseline = [
|
|
111
|
-
{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"},
|
|
112
|
-
{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"},
|
|
113
|
-
]
|
|
114
|
-
assert filter_new_findings([f1, f2], baseline) == []
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def test_multiset_one_extra_reported():
|
|
118
|
-
# Three identical in new run, two in baseline → one new.
|
|
119
|
-
f = _f("PROOFCTL-P-001", msg="'foo' body is pass")
|
|
120
|
-
baseline = [
|
|
121
|
-
{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"},
|
|
122
|
-
{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "'foo' body is pass"},
|
|
123
|
-
]
|
|
124
|
-
result = filter_new_findings([f, f, f], baseline)
|
|
125
|
-
assert len(result) == 1
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_mixed_findings():
|
|
129
|
-
f_old = _f("PROOFCTL-P-001", msg="old")
|
|
130
|
-
f_new = _f("PROOFCTL-Q-001", msg="new")
|
|
131
|
-
baseline = [{"rule_id": "PROOFCTL-P-001", "file": "foo.py", "message": "old"}]
|
|
132
|
-
result = filter_new_findings([f_old, f_new], baseline)
|
|
133
|
-
assert result == [f_new]
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"""Tests for proofctl config loading."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from proofctl.config import ProofctlConfig, load_config
|
|
9
|
-
from proofctl.models import Severity
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# ── defaults ──────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
def test_load_config_no_file_returns_defaults(tmp_path):
|
|
15
|
-
cfg = load_config(tmp_path)
|
|
16
|
-
assert isinstance(cfg, ProofctlConfig)
|
|
17
|
-
assert "**/.venv/**" in cfg.exclude_paths
|
|
18
|
-
assert cfg.disable == []
|
|
19
|
-
assert cfg.severity_overrides == {}
|
|
20
|
-
assert cfg.any_threshold == 3
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_default_excludes_include_vendor_generated_paths(tmp_path):
|
|
24
|
-
"""All vendor/generated tool paths must be present in default excludes."""
|
|
25
|
-
cfg = load_config(tmp_path)
|
|
26
|
-
required = [
|
|
27
|
-
# Flux CD bootstrap files
|
|
28
|
-
"**/flux-system/gotk-components.yaml",
|
|
29
|
-
"**/flux-system/gotk-sync.yaml",
|
|
30
|
-
# ArgoCD Autopilot bootstrap files
|
|
31
|
-
"**/bootstrap/argo-cd/kustomization.yaml",
|
|
32
|
-
"**/bootstrap/argo-cd.yaml",
|
|
33
|
-
# Terraform generated artifacts
|
|
34
|
-
"**/*.tfstate",
|
|
35
|
-
"**/*.tfplan",
|
|
36
|
-
# CDK tools
|
|
37
|
-
"**/cdk.out/**",
|
|
38
|
-
"**/.gen/**",
|
|
39
|
-
# Helm chart tarballs
|
|
40
|
-
"**/charts/*.tgz",
|
|
41
|
-
# Pulumi local backend
|
|
42
|
-
"**/.pulumi/**",
|
|
43
|
-
# Go vendor
|
|
44
|
-
"**/vendor/**",
|
|
45
|
-
# Python caches
|
|
46
|
-
"**/.tox/**",
|
|
47
|
-
"**/.pytest_cache/**",
|
|
48
|
-
"**/.mypy_cache/**",
|
|
49
|
-
]
|
|
50
|
-
for pat in required:
|
|
51
|
-
assert pat in cfg.exclude_paths, f"missing default exclude: {pat}"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_load_config_empty_file_returns_defaults(tmp_path):
|
|
55
|
-
(tmp_path / ".proofctl.yaml").write_text("")
|
|
56
|
-
cfg = load_config(tmp_path)
|
|
57
|
-
assert cfg.any_threshold == 3
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# ── overrides ─────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
def test_load_config_disable_rule(tmp_path):
|
|
63
|
-
(tmp_path / ".proofctl.yaml").write_text("disable:\n - PROOFCTL-P-001\n")
|
|
64
|
-
cfg = load_config(tmp_path)
|
|
65
|
-
assert cfg.is_disabled("PROOFCTL-P-001")
|
|
66
|
-
assert not cfg.is_disabled("PROOFCTL-P-002")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_load_config_severity_override(tmp_path):
|
|
70
|
-
yaml = "severity_overrides:\n PROOFCTL-P-001: ERROR\n"
|
|
71
|
-
(tmp_path / ".proofctl.yaml").write_text(yaml)
|
|
72
|
-
cfg = load_config(tmp_path)
|
|
73
|
-
assert cfg.effective_severity("PROOFCTL-P-001", Severity.WARNING) == Severity.ERROR
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_load_config_severity_override_invalid_raises_at_load(tmp_path):
|
|
77
|
-
"""AUDIT H-8: bad severity_overrides values must fail at load_config(),
|
|
78
|
-
not silently propagate to the first finding (which had no file context)."""
|
|
79
|
-
yaml = "severity_overrides:\n PROOFCTL-P-001: BOGUS\n"
|
|
80
|
-
(tmp_path / ".proofctl.yaml").write_text(yaml)
|
|
81
|
-
with pytest.raises(ValueError, match="invalid"):
|
|
82
|
-
load_config(tmp_path)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_load_config_exclude_paths_override(tmp_path):
|
|
86
|
-
(tmp_path / ".proofctl.yaml").write_text("exclude_paths:\n - '**/generated/**'\n")
|
|
87
|
-
cfg = load_config(tmp_path)
|
|
88
|
-
assert "**/generated/**" in cfg.exclude_paths
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# ── rule-level config ─────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
def test_load_config_p002_any_threshold(tmp_path):
|
|
94
|
-
yaml = "rules:\n PROOFCTL-Q-003:\n any_threshold: 5\n"
|
|
95
|
-
(tmp_path / ".proofctl.yaml").write_text(yaml)
|
|
96
|
-
cfg = load_config(tmp_path)
|
|
97
|
-
assert cfg.any_threshold == 5
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_load_config_p002_todo_allowed(tmp_path):
|
|
101
|
-
yaml = "rules:\n PROOFCTL-P-002:\n allowed_formats:\n - 'TODO\\(#\\d+\\)'\n"
|
|
102
|
-
(tmp_path / ".proofctl.yaml").write_text(yaml)
|
|
103
|
-
cfg = load_config(tmp_path)
|
|
104
|
-
assert len(cfg.todo_allowed_formats) == 1
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# ── is_disabled ───────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
def test_is_disabled_true():
|
|
110
|
-
cfg = ProofctlConfig(disable=["PROOFCTL-S-001"])
|
|
111
|
-
assert cfg.is_disabled("PROOFCTL-S-001")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_is_disabled_false():
|
|
115
|
-
cfg = ProofctlConfig(disable=["PROOFCTL-S-001"])
|
|
116
|
-
assert not cfg.is_disabled("PROOFCTL-S-002")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_is_disabled_empty():
|
|
120
|
-
cfg = ProofctlConfig()
|
|
121
|
-
assert not cfg.is_disabled("ANYTHING")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# ── effective_severity ────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
def test_effective_severity_no_override_returns_default():
|
|
127
|
-
cfg = ProofctlConfig()
|
|
128
|
-
assert cfg.effective_severity("PROOFCTL-P-001", Severity.WARNING) == Severity.WARNING
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_effective_severity_override_applied():
|
|
132
|
-
cfg = ProofctlConfig(severity_overrides={"PROOFCTL-P-001": "ERROR"})
|
|
133
|
-
assert cfg.effective_severity("PROOFCTL-P-001", Severity.WARNING) == Severity.ERROR
|