proofctl 0.6.0__tar.gz → 0.6.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.
- {proofctl-0.6.0/proofctl.egg-info → proofctl-0.6.1}/PKG-INFO +5 -5
- {proofctl-0.6.0 → proofctl-0.6.1}/README.md +4 -4
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/terraform.py +85 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/yaml_checker.py +112 -0
- {proofctl-0.6.0 → proofctl-0.6.1/proofctl.egg-info}/PKG-INFO +5 -5
- {proofctl-0.6.0 → proofctl-0.6.1}/pyproject.toml +6 -4
- {proofctl-0.6.0 → proofctl-0.6.1}/LICENSE +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/MANIFEST.in +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/__init__.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/_globmatch.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/baseline.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/__init__.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/base.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/dockerfile.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/hcl_utils.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/imports.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/leakage.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/llm_integration.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/placeholders.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/quality.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/secrets.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/security.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/shell_checker.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/terragrunt.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/checkers/variants.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/cli.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/config.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/engine.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/fixer.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/models.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/registry.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/reporters/__init__.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/reporters/html_reporter.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/reporters/json_reporter.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl/reporters/terminal.py +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl.egg-info/SOURCES.txt +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl.egg-info/dependency_links.txt +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl.egg-info/entry_points.txt +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl.egg-info/requires.txt +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/proofctl.egg-info/top_level.txt +0 -0
- {proofctl-0.6.0 → proofctl-0.6.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofctl
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: DevOps linter and security scanner for AI slop and misconfigurations — runs in pre-commit, CI, or post-commit; no external tools required
|
|
5
5
|
Author: Kolawolu Odunola
|
|
6
6
|
License: MIT
|
|
@@ -35,7 +35,7 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
# proofctl
|
|
37
37
|
|
|
38
|
-
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **
|
|
38
|
+
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **271 rules**, no external tools, runs in under a second.
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
proofctl check .
|
|
@@ -172,7 +172,7 @@ git commit -m "chore: add proofctl baseline"
|
|
|
172
172
|
|
|
173
173
|
## Rules
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
271 rules across 13 families. Run `proofctl rules` after install to enumerate.
|
|
176
176
|
|
|
177
177
|
| Family | Count | What it covers |
|
|
178
178
|
|---|---|---|
|
|
@@ -182,12 +182,12 @@ git commit -m "chore: add proofctl baseline"
|
|
|
182
182
|
| Python — Other | 15 | Placeholders, leakage (cross-language idioms, PII), import hallucination, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
183
183
|
| Terraform — General + AI (`TF-T`, `TF-AI`) | 25 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
|
|
184
184
|
| Terraform — AWS (`TF-A`) | 36 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
|
|
185
|
-
| Terraform — GCP (`TF-G`) |
|
|
185
|
+
| Terraform — GCP (`TF-G`) | 48 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
|
|
186
186
|
| Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
|
|
187
187
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 10 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
188
188
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
189
189
|
| Dockerfile (`DF`) | 15 | Latest tag, root user, secret leakage, OCI labels, HEALTHCHECK, legacy ENV form |
|
|
190
|
-
| Kubernetes YAML (`YAML`) |
|
|
190
|
+
| Kubernetes YAML (`YAML`) | 34 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
|
|
191
191
|
| GitHub Actions (`YAML-GHA`) | 6 | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
|
|
192
192
|
| Shell (`SH`) | 8 | Dangerous globbing, missing `set -euo pipefail` |
|
|
193
193
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# proofctl
|
|
2
2
|
|
|
3
|
-
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **
|
|
3
|
+
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **271 rules**, no external tools, 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
|
+
271 rules across 13 families. Run `proofctl rules` after install to enumerate.
|
|
141
141
|
|
|
142
142
|
| Family | Count | What it covers |
|
|
143
143
|
|---|---|---|
|
|
@@ -147,12 +147,12 @@ git commit -m "chore: add proofctl baseline"
|
|
|
147
147
|
| Python — Other | 15 | Placeholders, leakage (cross-language idioms, PII), import hallucination, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
148
148
|
| Terraform — General + AI (`TF-T`, `TF-AI`) | 25 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
|
|
149
149
|
| Terraform — AWS (`TF-A`) | 36 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
|
|
150
|
-
| Terraform — GCP (`TF-G`) |
|
|
150
|
+
| Terraform — GCP (`TF-G`) | 48 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
|
|
151
151
|
| Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
|
|
152
152
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 10 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
153
153
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
154
154
|
| Dockerfile (`DF`) | 15 | Latest tag, root user, secret leakage, OCI labels, HEALTHCHECK, legacy ENV form |
|
|
155
|
-
| Kubernetes YAML (`YAML`) |
|
|
155
|
+
| Kubernetes YAML (`YAML`) | 34 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
|
|
156
156
|
| GitHub Actions (`YAML-GHA`) | 6 | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
|
|
157
157
|
| Shell (`SH`) | 8 | Dangerous globbing, missing `set -euo pipefail` |
|
|
158
158
|
|
|
@@ -121,6 +121,30 @@ _BROAD_GCP_ROLES = frozenset({
|
|
|
121
121
|
"roles/iam.serviceAccountAdmin",
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
# Project-scoped roles that grant data-plane access to EVERY resource of that
|
|
125
|
+
# service in the project. Resource-level IAM exists for all of them, so a
|
|
126
|
+
# project-wide grant is the cross-tenant blast-radius shape: one compromised
|
|
127
|
+
# principal reads (or rewrites) every tenant's data.
|
|
128
|
+
_PROJECT_WIDE_DATA_ROLES = frozenset({
|
|
129
|
+
"roles/secretmanager.admin",
|
|
130
|
+
"roles/secretmanager.secretAccessor",
|
|
131
|
+
"roles/secretmanager.secretVersionManager",
|
|
132
|
+
"roles/datastore.owner",
|
|
133
|
+
"roles/datastore.user",
|
|
134
|
+
"roles/datastore.viewer",
|
|
135
|
+
"roles/storage.objectAdmin",
|
|
136
|
+
"roles/storage.objectUser",
|
|
137
|
+
"roles/storage.objectViewer",
|
|
138
|
+
"roles/cloudkms.cryptoKeyDecrypter",
|
|
139
|
+
"roles/cloudkms.cryptoKeyEncrypterDecrypter",
|
|
140
|
+
"roles/bigquery.dataOwner",
|
|
141
|
+
"roles/bigquery.dataEditor",
|
|
142
|
+
"roles/bigquery.dataViewer",
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
# Any `roles/<service>.admin` at project scope (storage.admin, cloudsql.admin…).
|
|
146
|
+
_SERVICE_ADMIN_ROLE_RE = re.compile(r"^roles/[a-z][A-Za-z]*\.admin$")
|
|
147
|
+
|
|
124
148
|
_GCP_IAM_RESOURCES = frozenset({
|
|
125
149
|
"google_project_iam_binding",
|
|
126
150
|
"google_project_iam_member",
|
|
@@ -247,6 +271,8 @@ class TerraformChecker(HclFileChecker):
|
|
|
247
271
|
out += self._g019(path, blocks)
|
|
248
272
|
out += self._g020(path, blocks)
|
|
249
273
|
out += self._g021(path, blocks)
|
|
274
|
+
out += self._g049(path, blocks)
|
|
275
|
+
out += self._g050(path, blocks)
|
|
250
276
|
out += self._g023(path, blocks)
|
|
251
277
|
out += self._g024(path, blocks)
|
|
252
278
|
out += self._g025(path, blocks)
|
|
@@ -1359,6 +1385,65 @@ class TerraformChecker(HclFileChecker):
|
|
|
1359
1385
|
))
|
|
1360
1386
|
return out
|
|
1361
1387
|
|
|
1388
|
+
def _g049(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
1389
|
+
"""G-049 — Project-scoped role granting data access to every resource."""
|
|
1390
|
+
out = []
|
|
1391
|
+
for b in blocks:
|
|
1392
|
+
if b.kind != "resource" or b.label1 not in _GCP_IAM_RESOURCES:
|
|
1393
|
+
continue
|
|
1394
|
+
role = (b.attr_value("role") or "").strip('"')
|
|
1395
|
+
if not role or role in _BROAD_GCP_ROLES:
|
|
1396
|
+
continue # G-021 already covers the primitive/IAM-admin roles
|
|
1397
|
+
if role not in _PROJECT_WIDE_DATA_ROLES and not _SERVICE_ADMIN_ROLE_RE.match(role):
|
|
1398
|
+
continue
|
|
1399
|
+
out.append(self._f(
|
|
1400
|
+
path, b.attr_line("role") or b.start_line,
|
|
1401
|
+
"PROOFCTL-TF-G049", "Project-wide data-access role",
|
|
1402
|
+
Severity.WARNING,
|
|
1403
|
+
f'"{b.label1}" grants "{role}" at project scope — this principal can '
|
|
1404
|
+
f"reach every resource of that service in the project",
|
|
1405
|
+
hint=(
|
|
1406
|
+
"Bind the role on the specific resource instead — e.g. "
|
|
1407
|
+
"google_secret_manager_secret_iam_member / "
|
|
1408
|
+
"google_storage_bucket_iam_member — so one compromised principal "
|
|
1409
|
+
"cannot read every tenant's data."
|
|
1410
|
+
),
|
|
1411
|
+
authority="Google IAM — principle of least privilege",
|
|
1412
|
+
docs_url="https://cloud.google.com/iam/docs/best-practices-service-accounts",
|
|
1413
|
+
))
|
|
1414
|
+
return out
|
|
1415
|
+
|
|
1416
|
+
def _g050(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
1417
|
+
"""G-050 — Firewall rule allows unrestricted egress to the internet."""
|
|
1418
|
+
out = []
|
|
1419
|
+
for b in blocks:
|
|
1420
|
+
if b.kind != "resource" or b.label1 != "google_compute_firewall":
|
|
1421
|
+
continue
|
|
1422
|
+
if (b.attr_value("direction") or "").strip('"').upper() != "EGRESS":
|
|
1423
|
+
continue
|
|
1424
|
+
if not b.contains(r'destination_ranges\s*=\s*\[[^\]]*"0\.0\.0\.0/0"'):
|
|
1425
|
+
continue
|
|
1426
|
+
# A deny-all egress rule to 0.0.0.0/0 is the protective pattern, not a
|
|
1427
|
+
# finding. Match single-line and multi-line block forms.
|
|
1428
|
+
if b.has_nested("deny") or b.contains(r"\bdeny\s*\{"):
|
|
1429
|
+
continue
|
|
1430
|
+
if not (b.has_nested("allow") or b.contains(r"\ballow\s*\{")):
|
|
1431
|
+
continue
|
|
1432
|
+
out.append(self._f(
|
|
1433
|
+
path, b.start_line,
|
|
1434
|
+
"PROOFCTL-TF-G050", "Firewall allows egress to the internet",
|
|
1435
|
+
Severity.WARNING,
|
|
1436
|
+
f'google_compute_firewall "{b.label2}" allows egress to 0.0.0.0/0 — '
|
|
1437
|
+
f"a compromised workload can exfiltrate data anywhere",
|
|
1438
|
+
hint=(
|
|
1439
|
+
"Restrict destination_ranges to the CIDRs actually required, or keep "
|
|
1440
|
+
"an FQDN/CIDR allow-list above a low-priority deny-all egress rule."
|
|
1441
|
+
),
|
|
1442
|
+
authority="CIS GCP Benchmark 3.x / MITRE ATT&CK T1048",
|
|
1443
|
+
docs_url="https://cloud.google.com/vpc/docs/firewalls",
|
|
1444
|
+
))
|
|
1445
|
+
return out
|
|
1446
|
+
|
|
1362
1447
|
def _g023(self, path: Path, blocks: list[HclBlock]) -> list[Finding]:
|
|
1363
1448
|
"""G-023 — KMS key rotation not configured or > 90 days."""
|
|
1364
1449
|
out = []
|
|
@@ -812,6 +812,116 @@ def _yaml014_k8s(root: yaml.MappingNode, path: Path) -> list[Finding]:
|
|
|
812
812
|
return findings
|
|
813
813
|
|
|
814
814
|
|
|
815
|
+
def _netpol_meta(root: yaml.MappingNode) -> tuple[str, yaml.MappingNode | None]:
|
|
816
|
+
"""Return (name, spec) for a NetworkPolicy document, or ("", None)."""
|
|
817
|
+
if _scalar(_get_key(root, "kind")) != "NetworkPolicy":
|
|
818
|
+
return "", None
|
|
819
|
+
name = "unnamed"
|
|
820
|
+
meta = _get_key(root, "metadata")
|
|
821
|
+
if isinstance(meta, yaml.MappingNode):
|
|
822
|
+
name = _scalar(_get_key(meta, "name")) or "unnamed"
|
|
823
|
+
spec = _get_key(root, "spec")
|
|
824
|
+
return name, spec if isinstance(spec, yaml.MappingNode) else None
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _yaml027_netpol_no_egress(root: yaml.MappingNode, path: Path) -> list[Finding]:
|
|
828
|
+
"""PROOFCTL-YAML-027 — NetworkPolicy does not restrict egress.
|
|
829
|
+
|
|
830
|
+
Omitting `Egress` from policyTypes means Kubernetes defaults to
|
|
831
|
+
`policyTypes: [Ingress]`, leaving outbound traffic completely
|
|
832
|
+
unrestricted — the pod can still reach the cloud metadata endpoint
|
|
833
|
+
and any internet host.
|
|
834
|
+
"""
|
|
835
|
+
name, spec = _netpol_meta(root)
|
|
836
|
+
if spec is None:
|
|
837
|
+
return []
|
|
838
|
+
|
|
839
|
+
types_node = _get_key(spec, "policyTypes")
|
|
840
|
+
declared = []
|
|
841
|
+
if isinstance(types_node, yaml.SequenceNode):
|
|
842
|
+
declared = [_scalar(v) or "" for v in types_node.value]
|
|
843
|
+
if "Egress" in declared:
|
|
844
|
+
return []
|
|
845
|
+
# An explicit `egress:` block implies Egress even if policyTypes is absent.
|
|
846
|
+
if _get_key(spec, "egress") is not None:
|
|
847
|
+
return []
|
|
848
|
+
|
|
849
|
+
node = types_node or spec
|
|
850
|
+
return [Finding(
|
|
851
|
+
file=str(path),
|
|
852
|
+
line=node.start_mark.line + 1,
|
|
853
|
+
col=node.start_mark.column,
|
|
854
|
+
rule_id="PROOFCTL-YAML-027",
|
|
855
|
+
rule_name="NetworkPolicy does not restrict egress",
|
|
856
|
+
severity=Severity.WARNING,
|
|
857
|
+
message=(
|
|
858
|
+
f"NetworkPolicy '{name}' has no Egress policyType — outbound traffic is "
|
|
859
|
+
f"unrestricted, so a compromised pod can reach the metadata endpoint and "
|
|
860
|
+
f"exfiltrate to any host"
|
|
861
|
+
),
|
|
862
|
+
hint=(
|
|
863
|
+
"Add 'Egress' to policyTypes and an egress allow-list (DNS, the datastore "
|
|
864
|
+
"CIDR, required APIs). Default-deny egress also blocks 169.254.169.254."
|
|
865
|
+
),
|
|
866
|
+
authority="Kubernetes Network Policies / NSA-CISA Kubernetes Hardening",
|
|
867
|
+
docs_url="https://kubernetes.io/docs/concepts/services-networking/network-policies/",
|
|
868
|
+
)]
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _yaml028_netpol_broad_peer(root: yaml.MappingNode, path: Path) -> list[Finding]:
|
|
872
|
+
"""PROOFCTL-YAML-028 — NetworkPolicy peer selects a whole namespace, all ports.
|
|
873
|
+
|
|
874
|
+
A `from`/`to` peer carrying only a namespaceSelector admits every pod in any
|
|
875
|
+
matching namespace. With no `ports:` on the rule it is every port too, so any
|
|
876
|
+
workload landing in that namespace reaches the datastore tier directly.
|
|
877
|
+
"""
|
|
878
|
+
name, spec = _netpol_meta(root)
|
|
879
|
+
if spec is None:
|
|
880
|
+
return []
|
|
881
|
+
|
|
882
|
+
findings: list[Finding] = []
|
|
883
|
+
for direction, peer_key in (("ingress", "from"), ("egress", "to")):
|
|
884
|
+
rules_node = _get_key(spec, direction)
|
|
885
|
+
if not isinstance(rules_node, yaml.SequenceNode):
|
|
886
|
+
continue
|
|
887
|
+
for rule in rules_node.value:
|
|
888
|
+
if not isinstance(rule, yaml.MappingNode):
|
|
889
|
+
continue
|
|
890
|
+
has_ports = _get_key(rule, "ports") is not None
|
|
891
|
+
peers = _get_key(rule, peer_key)
|
|
892
|
+
if not isinstance(peers, yaml.SequenceNode):
|
|
893
|
+
continue
|
|
894
|
+
for peer in peers.value:
|
|
895
|
+
if not isinstance(peer, yaml.MappingNode):
|
|
896
|
+
continue
|
|
897
|
+
keys = {_scalar(k) for k, _ in peer.value}
|
|
898
|
+
# Only a namespaceSelector: no pod narrowing within the namespace.
|
|
899
|
+
if "namespaceSelector" not in keys or "podSelector" in keys:
|
|
900
|
+
continue
|
|
901
|
+
if has_ports:
|
|
902
|
+
continue # port-scoped: materially narrower, don't flag
|
|
903
|
+
findings.append(Finding(
|
|
904
|
+
file=str(path),
|
|
905
|
+
line=peer.start_mark.line + 1,
|
|
906
|
+
col=peer.start_mark.column,
|
|
907
|
+
rule_id="PROOFCTL-YAML-028",
|
|
908
|
+
rule_name="NetworkPolicy peer selects entire namespace",
|
|
909
|
+
severity=Severity.WARNING,
|
|
910
|
+
message=(
|
|
911
|
+
f"NetworkPolicy '{name}' {direction} rule admits every pod in the "
|
|
912
|
+
f"selected namespace on every port"
|
|
913
|
+
),
|
|
914
|
+
hint=(
|
|
915
|
+
"Pair the namespaceSelector with a podSelector in the same peer "
|
|
916
|
+
"(logical AND) and add a 'ports:' list, so only the intended "
|
|
917
|
+
"workload and port are reachable."
|
|
918
|
+
),
|
|
919
|
+
authority="Kubernetes Network Policies / NSA-CISA Kubernetes Hardening",
|
|
920
|
+
docs_url="https://kubernetes.io/docs/concepts/services-networking/network-policies/",
|
|
921
|
+
))
|
|
922
|
+
return findings
|
|
923
|
+
|
|
924
|
+
|
|
815
925
|
def _yaml015_k8s(root: yaml.MappingNode, path: Path) -> list[Finding]:
|
|
816
926
|
"""PROOFCTL-YAML-015 — ClusterRoleBinding with roleRef.name: cluster-admin."""
|
|
817
927
|
findings: list[Finding] = []
|
|
@@ -1793,6 +1903,8 @@ class _YamlCheckerImpl(YamlFileChecker):
|
|
|
1793
1903
|
findings.extend(_yaml024_runasuser_low(root, path))
|
|
1794
1904
|
findings.extend(_yaml025_rbac_wildcard_admin(root, path))
|
|
1795
1905
|
findings.extend(_yaml026_net_raw(root, path))
|
|
1906
|
+
findings.extend(_yaml027_netpol_no_egress(root, path))
|
|
1907
|
+
findings.extend(_yaml028_netpol_broad_peer(root, path))
|
|
1796
1908
|
# AI-slop: removed apiVersions, PSP, explicit root, deprecated annotations
|
|
1797
1909
|
findings.extend(_yaml_ai001_k8s_removed_api(root, path))
|
|
1798
1910
|
findings.extend(_yaml_ai002_pod_security_policy(root, path))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofctl
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: DevOps linter and security scanner for AI slop and misconfigurations — runs in pre-commit, CI, or post-commit; no external tools required
|
|
5
5
|
Author: Kolawolu Odunola
|
|
6
6
|
License: MIT
|
|
@@ -35,7 +35,7 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
# proofctl
|
|
37
37
|
|
|
38
|
-
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **
|
|
38
|
+
A static-analysis linter and security scanner for the DevOps stack — Python, Terraform, Kubernetes, Helm, Dockerfiles, GitHub Actions, Shell — that catches the specific kinds of mistakes AI coding tools introduce. **271 rules**, no external tools, runs in under a second.
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
proofctl check .
|
|
@@ -172,7 +172,7 @@ git commit -m "chore: add proofctl baseline"
|
|
|
172
172
|
|
|
173
173
|
## Rules
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
271 rules across 13 families. Run `proofctl rules` after install to enumerate.
|
|
176
176
|
|
|
177
177
|
| Family | Count | What it covers |
|
|
178
178
|
|---|---|---|
|
|
@@ -182,12 +182,12 @@ git commit -m "chore: add proofctl baseline"
|
|
|
182
182
|
| Python — Other | 15 | Placeholders, leakage (cross-language idioms, PII), import hallucination, variant detection, LLM guardrails (OWASP LLM Top-10) |
|
|
183
183
|
| Terraform — General + AI (`TF-T`, `TF-AI`) | 25 | required_version, sensitive outputs, IAM wildcards, mutable git refs, deprecated resources, AWS-region-in-GCP, placeholder values, image-family pinning, sensitive vars in VM metadata, unused variable declarations |
|
|
184
184
|
| Terraform — AWS (`TF-A`) | 36 | EC2 IMDSv2, S3 public-access-block, EBS encryption, VPC flow logs, KMS rotation, RDS hardening, SG / NACL / EKS / DynamoDB / ElastiCache / CloudTrail / IAM |
|
|
185
|
-
| Terraform — GCP (`TF-G`) |
|
|
185
|
+
| Terraform — GCP (`TF-G`) | 48 | GKE hardening, GCS public-access prevention, IAM roles, KMS rotation, Cloud Function/Run public, BigQuery, Pub/Sub CMEK, Cloud SQL (TLS ssl_mode aware), firewall ingress, compute access scopes, Secret Manager CMEK consistency |
|
|
186
186
|
| Terraform — Azure (`TF-AZ`) | 14 | Storage public blob, SQL public network, AKS RBAC, Key Vault purge protection, NSG SSH/RDP, managed disk CMK |
|
|
187
187
|
| Terraform — Mechanics + Lifecycle (`TF-M`, `TF-V`) | 10 | SG inline mixing, `force_destroy`, mutable module refs, lifecycle antipatterns |
|
|
188
188
|
| Terragrunt (`TG`) | 6 | HCL inputs, mock_outputs, remote state encryption |
|
|
189
189
|
| Dockerfile (`DF`) | 15 | Latest tag, root user, secret leakage, OCI labels, HEALTHCHECK, legacy ENV form |
|
|
190
|
-
| Kubernetes YAML (`YAML`) |
|
|
190
|
+
| Kubernetes YAML (`YAML`) | 34 | Pod Security Standards, hostPath, default namespace, image digest pinning, RBAC wildcards |
|
|
191
191
|
| GitHub Actions (`YAML-GHA`) | 6 | SHA pinning, expression injection, missing permissions, secret env-var leakage, missing timeouts |
|
|
192
192
|
| Shell (`SH`) | 8 | Dangerous globbing, missing `set -euo pipefail` |
|
|
193
193
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofctl"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "DevOps linter and security scanner for AI slop and misconfigurations — runs in pre-commit, CI, or post-commit; no external tools required"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -59,9 +59,11 @@ target-version = "py311"
|
|
|
59
59
|
line-length = 120
|
|
60
60
|
|
|
61
61
|
[tool.ruff.lint]
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
62
|
+
# Pinned explicitly, NOT left to ruff's default: the default set changes between
|
|
63
|
+
# ruff versions, and `ruff>=0.4` in [dev] would then silently move a hard CI gate.
|
|
64
|
+
# E4/E7/E9 + F are the bug-class rules (undefined names, unused imports/vars).
|
|
65
|
+
select = ["E4", "E7", "E9", "F"]
|
|
66
|
+
# E701 (compact one-line `if cond: return`) is intentional in the type-dispatch helpers.
|
|
65
67
|
ignore = ["E701"]
|
|
66
68
|
|
|
67
69
|
[tool.setuptools.packages.find]
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|