proofctl 0.2.0__tar.gz → 0.2.1__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 (69) hide show
  1. {proofctl-0.2.0 → proofctl-0.2.1}/PKG-INFO +1 -1
  2. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/terraform.py +8 -1
  3. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/config.py +59 -3
  4. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/engine.py +18 -1
  5. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/PKG-INFO +1 -1
  6. {proofctl-0.2.0 → proofctl-0.2.1}/pyproject.toml +1 -1
  7. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_config_proofctl.py +31 -0
  8. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_engine.py +68 -0
  9. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_terraform_ext.py +30 -0
  10. {proofctl-0.2.0 → proofctl-0.2.1}/LICENSE +0 -0
  11. {proofctl-0.2.0 → proofctl-0.2.1}/README.md +0 -0
  12. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/__init__.py +0 -0
  13. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/_globmatch.py +0 -0
  14. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/baseline.py +0 -0
  15. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/__init__.py +0 -0
  16. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/base.py +0 -0
  17. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/dockerfile.py +0 -0
  18. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/hcl_utils.py +0 -0
  19. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/imports.py +0 -0
  20. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/leakage.py +0 -0
  21. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/llm_integration.py +0 -0
  22. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/placeholders.py +0 -0
  23. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/quality.py +0 -0
  24. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/security.py +0 -0
  25. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/shell_checker.py +0 -0
  26. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/terragrunt.py +0 -0
  27. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/variants.py +0 -0
  28. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/checkers/yaml_checker.py +0 -0
  29. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/cli.py +0 -0
  30. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/fixer.py +0 -0
  31. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/models.py +0 -0
  32. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/registry.py +0 -0
  33. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/reporters/__init__.py +0 -0
  34. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/reporters/html_reporter.py +0 -0
  35. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/reporters/json_reporter.py +0 -0
  36. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl/reporters/terminal.py +0 -0
  37. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/SOURCES.txt +0 -0
  38. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/dependency_links.txt +0 -0
  39. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/entry_points.txt +0 -0
  40. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/requires.txt +0 -0
  41. {proofctl-0.2.0 → proofctl-0.2.1}/proofctl.egg-info/top_level.txt +0 -0
  42. {proofctl-0.2.0 → proofctl-0.2.1}/setup.cfg +0 -0
  43. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_baseline.py +0 -0
  44. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_dockerfile.py +0 -0
  45. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_fixer.py +0 -0
  46. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_imports.py +0 -0
  47. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_leakage.py +0 -0
  48. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_llm.py +0 -0
  49. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_local_packages.py +0 -0
  50. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase1_audit_fixes.py +0 -0
  51. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase2_audit_fixes.py +0 -0
  52. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase3_audit_fixes.py +0 -0
  53. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase4_audit_fixes.py +0 -0
  54. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase5_llm_imports_fixes.py +0 -0
  55. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase5_p_l_v_fixes.py +0 -0
  56. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase5_quality_fixes.py +0 -0
  57. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase6_audit_fixes.py +0 -0
  58. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_phase8_audit_fixes.py +0 -0
  59. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_placeholders.py +0 -0
  60. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_quality.py +0 -0
  61. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_quality_ext.py +0 -0
  62. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_security.py +0 -0
  63. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_security_ext.py +0 -0
  64. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_suppression.py +0 -0
  65. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_terraform.py +0 -0
  66. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_terragrunt.py +0 -0
  67. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_variants.py +0 -0
  68. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_yaml.py +0 -0
  69. {proofctl-0.2.0 → proofctl-0.2.1}/tests/test_yaml_k8s_gha.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofctl
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -1997,8 +1997,15 @@ class TerraformChecker(HclFileChecker):
1997
1997
 
1998
1998
  def _t015(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
1999
1999
  """T-015 — Output with sensitive-looking name but sensitive = true not set."""
2000
+ # "key" is excluded when followed by measurement/metadata suffixes
2001
+ # (e.g. max_key_age_days, key_count, key_rotation_period) — those are
2002
+ # not sensitive values, just numeric descriptors that happen to contain
2003
+ # the word "key". Other terms (password, secret, token, credential)
2004
+ # are always sensitive regardless of suffix.
2000
2005
  _sensitive_name_pat = re.compile(
2001
- r'(?:password|secret|token|key|credential)',
2006
+ r'(?:password|secret|token|credential'
2007
+ r'|key(?!_age|_days|_hours|_seconds|_count|_max|_min|_num'
2008
+ r'|_threshold|_expir|_rotation|_version|_size|_length))',
2002
2009
  re.IGNORECASE,
2003
2010
  )
2004
2011
  out = []
@@ -9,16 +9,72 @@ import yaml
9
9
  from .models import Severity
10
10
 
11
11
  _DEFAULT_EXCLUDES: list[str] = [
12
+ # ── Python project environments ───────────────────────────────────────────
12
13
  "**/.venv/**",
13
14
  "**/venv/**",
14
- "**/node_modules/**",
15
- "**/.git/**",
16
15
  "**/__pycache__/**",
17
16
  "**/*.egg-info/**",
18
17
  "**/dist/**",
19
18
  "**/build/**",
20
- "**/.terragrunt-cache/**",
19
+ # ── Universal VCS / package managers ─────────────────────────────────────
20
+ "**/.git/**",
21
+ "**/node_modules/**",
22
+ # ── Terraform / Terragrunt caches ─────────────────────────────────────────
21
23
  "**/.terraform/**",
24
+ "**/.terragrunt-cache/**",
25
+ # Terraform/OpenTofu state and plan artifacts — never hand-authored;
26
+ # may contain sensitive resource outputs and secrets.
27
+ "**/*.tfstate",
28
+ "**/*.tfstate.backup",
29
+ "**/*.tfplan",
30
+ # ── Flux CD ───────────────────────────────────────────────────────────────
31
+ # Both files are auto-generated by `flux bootstrap` and contain the comment
32
+ # "# This manifest was generated by flux. DO NOT EDIT."
33
+ # gotk-components.yaml carries wildcard RBAC + cluster-admin bindings that
34
+ # are intentional Flux design; gotk-sync.yaml wires the GitRepository/
35
+ # Kustomization that reconciles flux-system itself. Neither is user policy.
36
+ "**/flux-system/gotk-components.yaml",
37
+ "**/flux-system/gotk-sync.yaml",
38
+ # ── ArgoCD Autopilot ──────────────────────────────────────────────────────
39
+ # Files generated by `argocd-autopilot repo bootstrap`. The kustomization
40
+ # references upstream install.yaml which carries a cluster-admin binding for
41
+ # argocd-application-controller — intentional, not a misconfiguration.
42
+ "**/bootstrap/argo-cd/kustomization.yaml",
43
+ "**/bootstrap/argo-cd.yaml",
44
+ "**/bootstrap/cluster-resources/in-cluster/**",
45
+ # ── AWS CDK / CDK for Terraform ───────────────────────────────────────────
46
+ # cdk.out/ is synthesized CloudFormation JSON from `cdk synth`.
47
+ # .gen/ and cdktf.out/ are CDKTF provider bindings and synth output.
48
+ "**/cdk.out/**",
49
+ "**/.gen/**",
50
+ "**/cdktf.out/**",
51
+ # ── Helm ─────────────────────────────────────────────────────────────────
52
+ # Chart tarballs downloaded by `helm dep update` into charts/ subdirectories.
53
+ "**/charts/*.tgz",
54
+ # ── Pulumi ───────────────────────────────────────────────────────────────
55
+ # Local backend state written by `pulumi up` when using file:// backend.
56
+ "**/.pulumi/**",
57
+ # ── Other IaC tool caches ─────────────────────────────────────────────────
58
+ "**/packer_cache/**", # Packer: ISO/VM template cache (default ./packer_cache/)
59
+ "**/.vagrant/**", # Vagrant: machine state, private keys, VM references
60
+ "**/.crossplane/**", # Crossplane CLI: provider OCI layer / CRD schema cache
61
+ # ── Ansible Galaxy ────────────────────────────────────────────────────────
62
+ # Collections installed via `ansible-galaxy collection install`.
63
+ "**/collections/ansible_collections/**",
64
+ # ── Go vendor ─────────────────────────────────────────────────────────────
65
+ # Third-party module source vendored by `go mod vendor`. Contains .go files
66
+ # that can trigger false-positives in import / quality rules.
67
+ "**/vendor/**",
68
+ # ── Python tool caches ────────────────────────────────────────────────────
69
+ # These directories contain .py files and can surface false-positives if
70
+ # scanned (e.g. generated test helpers, mypy stubs, hypothesis examples).
71
+ "**/.tox/**",
72
+ "**/.nox/**",
73
+ "**/.pytest_cache/**",
74
+ "**/.mypy_cache/**",
75
+ "**/.ruff_cache/**",
76
+ "**/.hypothesis/**",
77
+ "**/htmlcov/**",
22
78
  ]
23
79
 
24
80
  # Top-level config keys recognised by `.proofctl.yaml`. Anything else triggers
@@ -411,7 +411,24 @@ def run_check(
411
411
  floor = Severity.from_str(min_severity)
412
412
  findings = [f for f in findings if f.severity >= floor]
413
413
 
414
- return sorted(findings, key=lambda x: (-x.severity, x.file, x.line or 0))
414
+ sorted_findings = sorted(findings, key=lambda x: (-x.severity, x.file, x.line or 0))
415
+
416
+ # ── 8. Module-level rule deduplication. ───────────────────────────────────
417
+ # Rules like T014 (missing required_version) represent a per-module property,
418
+ # not a per-file one. A module with 10 .tf files should produce one finding,
419
+ # not ten. After sorting we keep the first occurrence per (dir, rule_id) so
420
+ # the preserved finding is always the alphabetically-first file in the module.
421
+ _MODULE_LEVEL_RULES: frozenset[str] = frozenset({"PROOFCTL-TF-T014"})
422
+ seen_module_dirs: set[tuple[str, str]] = set()
423
+ result: list[Finding] = []
424
+ for f in sorted_findings:
425
+ if f.rule_id in _MODULE_LEVEL_RULES:
426
+ dir_key = (str(Path(f.file).parent), f.rule_id)
427
+ if dir_key in seen_module_dirs:
428
+ continue
429
+ seen_module_dirs.add(dir_key)
430
+ result.append(f)
431
+ return result
415
432
 
416
433
 
417
434
  def _normalise(f: Finding, root: Path, config: ProofctlConfig) -> Finding | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofctl
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofctl"
7
- version = "0.2.0"
7
+ version = "0.2.1"
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" }
@@ -20,6 +20,37 @@ def test_load_config_no_file_returns_defaults(tmp_path):
20
20
  assert cfg.any_threshold == 3
21
21
 
22
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
+
23
54
  def test_load_config_empty_file_returns_defaults(tmp_path):
24
55
  (tmp_path / ".proofctl.yaml").write_text("")
25
56
  cfg = load_config(tmp_path)
@@ -48,6 +48,57 @@ def test_engine_excludes_configured_paths(tmp_path):
48
48
  assert all("venv" not in f.file for f in findings)
49
49
 
50
50
 
51
+ _WILDCARD_RBAC_YAML = (
52
+ "apiVersion: rbac.authorization.k8s.io/v1\n"
53
+ "kind: ClusterRole\n"
54
+ "metadata:\n"
55
+ " name: crd-controller-flux-system\n"
56
+ "rules:\n"
57
+ "- apiGroups: ['*']\n"
58
+ " resources: ['*']\n"
59
+ " verbs: ['*']\n"
60
+ )
61
+
62
+
63
+ def test_engine_excludes_flux_gotk_components(tmp_path):
64
+ """Flux bootstrap gotk-components.yaml must be skipped by default (vendor file)."""
65
+ flux_dir = tmp_path / "clusters" / "prod" / "flux-system"
66
+ flux_dir.mkdir(parents=True)
67
+ (flux_dir / "gotk-components.yaml").write_text(_WILDCARD_RBAC_YAML)
68
+ findings = run_check(tmp_path, _cfg(), families=["YAML"])
69
+ assert not any("gotk-components" in f.file for f in findings)
70
+
71
+
72
+ def test_engine_excludes_flux_gotk_sync(tmp_path):
73
+ """Flux bootstrap gotk-sync.yaml must be skipped by default."""
74
+ flux_dir = tmp_path / "flux-system"
75
+ flux_dir.mkdir(parents=True)
76
+ (flux_dir / "gotk-sync.yaml").write_text(
77
+ "apiVersion: source.toolkit.fluxcd.io/v1\n"
78
+ "kind: GitRepository\n"
79
+ "metadata:\n"
80
+ " name: flux-system\n"
81
+ " namespace: flux-system\n"
82
+ )
83
+ findings = run_check(tmp_path, _cfg(), families=["YAML"])
84
+ assert not any("gotk-sync" in f.file for f in findings)
85
+
86
+
87
+ def test_engine_excludes_argocd_autopilot_bootstrap(tmp_path):
88
+ """ArgoCD Autopilot bootstrap files must be skipped by default (vendor RBAC)."""
89
+ bootstrap_dir = tmp_path / "bootstrap" / "argo-cd"
90
+ bootstrap_dir.mkdir(parents=True)
91
+ (bootstrap_dir / "kustomization.yaml").write_text(_WILDCARD_RBAC_YAML)
92
+ (tmp_path / "bootstrap" / "argo-cd.yaml").write_text(
93
+ "apiVersion: argoproj.io/v1alpha1\nkind: Application\n"
94
+ "metadata:\n name: argo-cd\n namespace: argocd\n"
95
+ )
96
+ findings = run_check(tmp_path, _cfg(), families=["YAML"])
97
+ assert not any(
98
+ "bootstrap/argo-cd" in f.file.replace("\\", "/") for f in findings
99
+ )
100
+
101
+
51
102
  def test_engine_respects_disabled_rules(tmp_path):
52
103
  """Rules in config.disable must produce zero findings."""
53
104
  (tmp_path / "code.py").write_text(
@@ -86,3 +137,20 @@ def test_engine_deduplicates_findings(tmp_path):
86
137
  findings = run_check(tmp_path, _cfg(), families=["S"])
87
138
  keys = [f.dedup_key() for f in findings]
88
139
  assert len(keys) == len(set(keys))
140
+
141
+
142
+ def test_engine_t014_deduplicates_to_one_per_module_directory(tmp_path):
143
+ """T014 must fire once per module directory, not once per .tf file."""
144
+ module = tmp_path / "atlantis"
145
+ module.mkdir()
146
+ # Three .tf files with resources but no required_version in any of them.
147
+ # HCL parser requires multi-line block syntax.
148
+ resource_tmpl = 'resource "google_storage_bucket" "b{n}" {{\n name = "b{n}"\n}}\n'
149
+ for name in ("apigateway.tf", "asg.tf", "cloudfunction.tf"):
150
+ (module / name).write_text(resource_tmpl.format(n=name[0]))
151
+ findings = run_check(tmp_path, _cfg(), families=["TF"])
152
+ t014 = [f for f in findings if f.rule_id == "PROOFCTL-TF-T014"]
153
+ assert len(t014) == 1, (
154
+ f"Expected 1 T014 finding for the module, got {len(t014)}: "
155
+ + ", ".join(f.file for f in t014)
156
+ )
@@ -1119,3 +1119,33 @@ output "encryption_key" {
1119
1119
  """
1120
1120
  f = _find(_checker.check(_P, src), "PROOFCTL-TF-T015")
1121
1121
  assert len(f) == 1
1122
+
1123
+
1124
+ def test_t015_key_age_days_is_not_sensitive():
1125
+ """max_key_age_days is a numeric duration, not a sensitive value (FP regression)."""
1126
+ src = """
1127
+ output "max_key_age_days" {
1128
+ value = 90
1129
+ }
1130
+ """
1131
+ assert "PROOFCTL-TF-T015" not in _ids(_checker.check(_P, src))
1132
+
1133
+
1134
+ def test_t015_key_count_is_not_sensitive():
1135
+ src = """
1136
+ output "key_rotation_days" {
1137
+ value = 30
1138
+ }
1139
+ """
1140
+ assert "PROOFCTL-TF-T015" not in _ids(_checker.check(_P, src))
1141
+
1142
+
1143
+ def test_t015_key_name_still_fires():
1144
+ """Output 'key_name' references a key resource — should still be flagged."""
1145
+ src = """
1146
+ output "kms_key_name" {
1147
+ value = google_kms_crypto_key.main.name
1148
+ }
1149
+ """
1150
+ f = _find(_checker.check(_P, src), "PROOFCTL-TF-T015")
1151
+ assert len(f) == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes