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.
Files changed (73) hide show
  1. {proofctl-0.4.1 → proofctl-0.4.3}/LICENSE +1 -1
  2. proofctl-0.4.3/MANIFEST.in +19 -0
  3. {proofctl-0.4.1 → proofctl-0.4.3}/PKG-INFO +5 -5
  4. {proofctl-0.4.1 → proofctl-0.4.3}/README.md +4 -4
  5. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/terraform.py +179 -0
  6. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/PKG-INFO +5 -5
  7. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/SOURCES.txt +2 -33
  8. {proofctl-0.4.1 → proofctl-0.4.3}/pyproject.toml +1 -1
  9. proofctl-0.4.1/tests/test_baseline.py +0 -133
  10. proofctl-0.4.1/tests/test_config_proofctl.py +0 -133
  11. proofctl-0.4.1/tests/test_dockerfile.py +0 -473
  12. proofctl-0.4.1/tests/test_engine.py +0 -191
  13. proofctl-0.4.1/tests/test_fixer.py +0 -163
  14. proofctl-0.4.1/tests/test_imports.py +0 -312
  15. proofctl-0.4.1/tests/test_leakage.py +0 -179
  16. proofctl-0.4.1/tests/test_llm.py +0 -279
  17. proofctl-0.4.1/tests/test_local_packages.py +0 -239
  18. proofctl-0.4.1/tests/test_phase1_audit_fixes.py +0 -176
  19. proofctl-0.4.1/tests/test_phase2_audit_fixes.py +0 -311
  20. proofctl-0.4.1/tests/test_phase3_audit_fixes.py +0 -305
  21. proofctl-0.4.1/tests/test_phase4_audit_fixes.py +0 -416
  22. proofctl-0.4.1/tests/test_phase5_llm_imports_fixes.py +0 -306
  23. proofctl-0.4.1/tests/test_phase5_p_l_v_fixes.py +0 -279
  24. proofctl-0.4.1/tests/test_phase5_quality_fixes.py +0 -240
  25. proofctl-0.4.1/tests/test_phase6_audit_fixes.py +0 -359
  26. proofctl-0.4.1/tests/test_phase8_audit_fixes.py +0 -328
  27. proofctl-0.4.1/tests/test_placeholders.py +0 -239
  28. proofctl-0.4.1/tests/test_quality.py +0 -595
  29. proofctl-0.4.1/tests/test_quality_ext.py +0 -286
  30. proofctl-0.4.1/tests/test_secrets.py +0 -512
  31. proofctl-0.4.1/tests/test_security.py +0 -509
  32. proofctl-0.4.1/tests/test_security_ext.py +0 -476
  33. proofctl-0.4.1/tests/test_security_external.py +0 -546
  34. proofctl-0.4.1/tests/test_suppression.py +0 -137
  35. proofctl-0.4.1/tests/test_terraform.py +0 -861
  36. proofctl-0.4.1/tests/test_terraform_ext.py +0 -2806
  37. proofctl-0.4.1/tests/test_terragrunt.py +0 -313
  38. proofctl-0.4.1/tests/test_variants.py +0 -136
  39. proofctl-0.4.1/tests/test_yaml.py +0 -469
  40. proofctl-0.4.1/tests/test_yaml_k8s_gha.py +0 -1387
  41. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/__init__.py +0 -0
  42. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/_globmatch.py +0 -0
  43. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/baseline.py +0 -0
  44. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/__init__.py +0 -0
  45. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/base.py +0 -0
  46. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/dockerfile.py +0 -0
  47. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/hcl_utils.py +0 -0
  48. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/imports.py +0 -0
  49. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/leakage.py +0 -0
  50. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/llm_integration.py +0 -0
  51. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/placeholders.py +0 -0
  52. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/quality.py +0 -0
  53. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/secrets.py +0 -0
  54. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/security.py +0 -0
  55. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/shell_checker.py +0 -0
  56. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/terragrunt.py +0 -0
  57. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/variants.py +0 -0
  58. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/checkers/yaml_checker.py +0 -0
  59. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/cli.py +0 -0
  60. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/config.py +0 -0
  61. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/engine.py +0 -0
  62. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/fixer.py +0 -0
  63. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/models.py +0 -0
  64. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/registry.py +0 -0
  65. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/__init__.py +0 -0
  66. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/html_reporter.py +0 -0
  67. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/json_reporter.py +0 -0
  68. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl/reporters/terminal.py +0 -0
  69. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/dependency_links.txt +0 -0
  70. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/entry_points.txt +0 -0
  71. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/requires.txt +0 -0
  72. {proofctl-0.4.1 → proofctl-0.4.3}/proofctl.egg-info/top_level.txt +0 -0
  73. {proofctl-0.4.1 → proofctl-0.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Kolawolu Odunola
3
+ Copyright (c) 2025-2026 Kolawolu Odunola
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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.1
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. **264 rules**, zero dependencies, runs in under a second.
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
- 264 rules across 14 families. Run `proofctl rules` after install to enumerate.
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`) | 26 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values |
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`) | 50 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress |
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. **264 rules**, zero dependencies, runs in under a second.
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
- 264 rules across 14 families. Run `proofctl rules` after install to enumerate.
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`) | 26 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values |
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`) | 50 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress |
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.1
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. **264 rules**, zero dependencies, runs in under a second.
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
- 264 rules across 14 families. Run `proofctl rules` after install to enumerate.
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`) | 26 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values |
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`) | 50 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL, firewall ingress |
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.1"
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