preflight-engine 0.1.0__py3-none-any.whl

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.
preflight/policy.py ADDED
@@ -0,0 +1,67 @@
1
+ """Policy evaluation engine: Enforces configurable deployment gates."""
2
+
3
+ from preflight.models import Finding, PolicyEvaluation, Severity
4
+
5
+
6
+ class PolicyEvaluator:
7
+ """Evaluates findings and risk scores against configured policy rules."""
8
+
9
+ def __init__(
10
+ self,
11
+ fail_on_severity: Severity | None = None,
12
+ blocked_rules: list[str] | None = None,
13
+ allowed_rules: list[str] | None = None,
14
+ max_allowed_score: float | None = None,
15
+ ) -> None:
16
+ self.fail_on_severity = fail_on_severity
17
+ self.blocked_rules = set(blocked_rules or [])
18
+ self.allowed_rules = set(allowed_rules or [])
19
+ self.max_allowed_score = max_allowed_score
20
+
21
+ def evaluate(
22
+ self,
23
+ findings: list[Finding],
24
+ overall_risk_score: float,
25
+ ) -> PolicyEvaluation:
26
+ """Evaluate policy gates against discovered findings and overall score."""
27
+ blocking_reasons: list[str] = []
28
+ triggered_rule_ids: set[str] = set()
29
+
30
+ # Filter out findings explicitly suppressed by allowed_rules
31
+ active_findings = [f for f in findings if f.rule_id not in self.allowed_rules]
32
+
33
+ # 1. Check severity threshold
34
+ if self.fail_on_severity is not None:
35
+ violating_findings = [f for f in active_findings if f.severity >= self.fail_on_severity]
36
+ for f in violating_findings:
37
+ triggered_rule_ids.add(f.rule_id)
38
+ blocking_reasons.append(
39
+ f"Finding '{f.rule_id}' on '{f.resource_address}' has severity '{f.severity.value}' "
40
+ f"which meets or exceeds policy threshold '{self.fail_on_severity.value}'."
41
+ )
42
+
43
+ # 2. Check explicitly blocked rule IDs
44
+ if self.blocked_rules:
45
+ for f in active_findings:
46
+ if f.rule_id in self.blocked_rules:
47
+ triggered_rule_ids.add(f.rule_id)
48
+ reason = f"Rule '{f.rule_id}' on '{f.resource_address}' is explicitly blocked by policy."
49
+ if reason not in blocking_reasons:
50
+ blocking_reasons.append(reason)
51
+
52
+ # 3. Check maximum allowed score threshold
53
+ if self.max_allowed_score is not None:
54
+ if overall_risk_score > self.max_allowed_score:
55
+ blocking_reasons.append(
56
+ f"Overall plan risk score ({overall_risk_score:.1f}) exceeds configured "
57
+ f"maximum allowed score ({self.max_allowed_score:.1f})."
58
+ )
59
+
60
+ blocked = len(blocking_reasons) > 0
61
+
62
+ return PolicyEvaluation(
63
+ blocked=blocked,
64
+ blocking_reasons=blocking_reasons,
65
+ fail_on_severity=self.fail_on_severity,
66
+ blocked_rule_ids=sorted(triggered_rule_ids),
67
+ )
preflight/redaction.py ADDED
@@ -0,0 +1,54 @@
1
+ """Defensive redaction of sensitive values from Terraform plans and findings."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ # Attribute name patterns that indicate sensitive or secret data
7
+ SENSITIVE_KEY_PATTERNS = re.compile(
8
+ r"(password|secret|token|api_key|access_key|private_key|credential|auth|"
9
+ r"cert|bearer|master_key|client_secret|ssh_key|encryption_key)",
10
+ re.IGNORECASE,
11
+ )
12
+
13
+ REDACTED_PLACEHOLDER = "[REDACTED]"
14
+
15
+
16
+ def is_sensitive_key(key: str) -> bool:
17
+ """Check if an attribute key matches sensitive naming patterns."""
18
+ return bool(SENSITIVE_KEY_PATTERNS.search(key))
19
+
20
+
21
+ def redact_sensitive_data(
22
+ data: Any,
23
+ sensitive_marks: Any = None,
24
+ ) -> Any:
25
+ """Recursively redact sensitive values from a dictionary, list, or primitive.
26
+
27
+ Respects both known sensitive key names and Terraform's sensitive_values metadata.
28
+ """
29
+ if sensitive_marks is True:
30
+ return REDACTED_PLACEHOLDER
31
+
32
+ if isinstance(data, dict):
33
+ redacted_dict: dict[str, Any] = {}
34
+ for key, value in data.items():
35
+ child_marks = sensitive_marks.get(key) if isinstance(sensitive_marks, dict) else None
36
+
37
+ if child_marks is True or is_sensitive_key(str(key)):
38
+ redacted_dict[key] = REDACTED_PLACEHOLDER
39
+ else:
40
+ redacted_dict[key] = redact_sensitive_data(value, child_marks)
41
+ return redacted_dict
42
+
43
+ if isinstance(data, list):
44
+ redacted_list: list[Any] = []
45
+ for index, item in enumerate(data):
46
+ child_marks = (
47
+ sensitive_marks[index]
48
+ if isinstance(sensitive_marks, list) and index < len(sensitive_marks)
49
+ else None
50
+ )
51
+ redacted_list.append(redact_sensitive_data(item, child_marks))
52
+ return redacted_list
53
+
54
+ return data
preflight/reporting.py ADDED
@@ -0,0 +1,404 @@
1
+ """Reporting layer: Renders terminal, JSON, Markdown, SARIF, and PR comment reports."""
2
+
3
+ import json
4
+ from io import StringIO
5
+ from typing import Any, TextIO
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+ from preflight import __version__
13
+ from preflight.models import (
14
+ AnalysisResult,
15
+ RiskCategory,
16
+ Severity,
17
+ )
18
+
19
+ PR_COMMENT_MARKER = "<!-- preflight-comment-marker -->"
20
+
21
+ # Color palettes for severities and risk bands
22
+ SEVERITY_STYLES = {
23
+ Severity.CRITICAL: "bold white on red",
24
+ Severity.HIGH: "bold red",
25
+ Severity.MODERATE: "bold yellow",
26
+ Severity.LOW: "bold cyan",
27
+ }
28
+
29
+ RISK_CATEGORY_STYLES = {
30
+ RiskCategory.CRITICAL: "bold red",
31
+ RiskCategory.HIGH: "bold orange3",
32
+ RiskCategory.MODERATE: "bold yellow",
33
+ RiskCategory.LOW: "bold green",
34
+ }
35
+
36
+
37
+ def render_terminal_report(result: AnalysisResult, file: TextIO | None = None) -> None:
38
+ """Render a clean, human-readable terminal report with Rich."""
39
+ console = Console(file=file, highlight=False, force_terminal=False if file else None)
40
+
41
+ # 1. Header Banner
42
+ cat_style = RISK_CATEGORY_STYLES.get(result.overall_risk_category, "bold white")
43
+ score_text = (
44
+ f"{result.overall_risk_score:.1f} / 100 ({result.overall_risk_category.value.upper()})"
45
+ )
46
+
47
+ header_text = Text()
48
+ header_text.append("PREFLIGHT: Infrastructure Change Risk Analysis\n", style="bold cyan")
49
+ header_text.append(f"Plan: {result.plan_path}\n", style="dim")
50
+ header_text.append(f"Environment: {result.environment.value.upper()}\n", style="dim")
51
+ header_text.append("Risk Score: ")
52
+ header_text.append(score_text, style=cat_style)
53
+
54
+ console.print(Panel(header_text, border_style="dim", expand=False))
55
+
56
+ # 2. Plan Actions Summary Table
57
+ summary = result.summary
58
+ summary_table = Table(title="Planned Resource Operations", border_style="dim", show_header=True)
59
+ summary_table.add_column("Create", style="green", justify="center")
60
+ summary_table.add_column("Update", style="yellow", justify="center")
61
+ summary_table.add_column("Delete", style="red", justify="center")
62
+ summary_table.add_column("Replace", style="bold red", justify="center")
63
+ summary_table.add_column("Read / No-op", style="dim", justify="center")
64
+ summary_table.add_column("Unknown Actions", style="magenta", justify="center")
65
+
66
+ summary_table.add_row(
67
+ str(summary.to_create),
68
+ str(summary.to_update),
69
+ str(summary.to_delete),
70
+ str(summary.to_replace),
71
+ f"{summary.to_read} / {summary.to_noop}",
72
+ str(summary.unknown_actions),
73
+ )
74
+ console.print(summary_table)
75
+
76
+ # 3. Findings Section
77
+ if not result.findings:
78
+ console.print(
79
+ "\n[bold green][PASS] No risk findings detected.[/bold green] Infrastructure changes appear safe.\n"
80
+ )
81
+ else:
82
+ console.print(f"\n[bold]Risk Findings ({len(result.findings)}):[/bold]\n")
83
+
84
+ for idx, finding in enumerate(result.findings, start=1):
85
+ sev_style = SEVERITY_STYLES.get(finding.severity, "bold white")
86
+ f_title = Text()
87
+ f_title.append(f"#{idx} [{finding.rule_id}] ", style="bold")
88
+ f_title.append(f" {finding.severity.value.upper()} ", style=sev_style)
89
+ f_title.append(f" {finding.rule_name} (Score: {finding.calculated_score:.1f})")
90
+
91
+ body = Text()
92
+ body.append("Resource: ", style="bold")
93
+ body.append(f"{finding.resource_address} ({finding.resource_type})\n")
94
+ body.append("Action: ", style="bold")
95
+ body.append(f"{finding.action.value.upper()}\n\n")
96
+
97
+ body.append("What changed:\n", style="bold")
98
+ body.append(f" {finding.what_changed}\n\n")
99
+
100
+ body.append("Why it matters:\n", style="bold")
101
+ body.append(f" {finding.why_it_matters}\n\n")
102
+
103
+ # Risk factors breakdown
104
+ rf = finding.risk_factors
105
+ body.append("Risk Factor Breakdown:\n", style="bold")
106
+ body.append(
107
+ f" Change Impact: {rf.change_impact:.1f}/5 | "
108
+ f"Security Exposure: {rf.security_exposure:.1f}/5 | "
109
+ f"Reversibility: {rf.reversibility:.1f}/5\n"
110
+ f" Env Criticality: {rf.environment_criticality:.1f}/5 | "
111
+ f"Uncertainty: {rf.uncertainty:.1f}/5\n\n",
112
+ style="cyan",
113
+ )
114
+
115
+ body.append("Remediation / Next Steps:\n", style="bold")
116
+ body.append(f" {finding.remediation}\n", style="green")
117
+
118
+ console.print(Panel(body, title=f_title, border_style="dim", expand=False))
119
+
120
+ # 4. Policy Gate Status
121
+ policy = result.policy_evaluation
122
+ if policy.blocked:
123
+ gate_text = Text()
124
+ gate_text.append("DEPLOYMENT BLOCKED BY POLICY\n", style="bold red")
125
+ for reason in policy.blocking_reasons:
126
+ gate_text.append(f"- {reason}\n", style="red")
127
+ console.print(Panel(gate_text, border_style="red", expand=False))
128
+ else:
129
+ status_msg = "PASSED: No configured policy conditions were violated."
130
+ console.print(
131
+ Panel(Text(status_msg, style="bold green"), border_style="green", expand=False)
132
+ )
133
+
134
+
135
+ def render_json_report(result: AnalysisResult) -> str:
136
+ """Render a deterministic, redacted JSON report."""
137
+ return json.dumps(result.to_dict(), indent=2, sort_keys=False)
138
+
139
+
140
+ def render_markdown_report(result: AnalysisResult) -> str:
141
+ """Render a sanitized Markdown report suitable for GitHub PR comments and CI summaries."""
142
+ lines: list[str] = []
143
+
144
+ # Title & Badge
145
+ lines.append("# Preflight Infrastructure Risk Report\n")
146
+
147
+ policy = result.policy_evaluation
148
+ if policy.blocked:
149
+ lines.append("> **Deployment Blocked by Preflight Policy**\n")
150
+ for reason in policy.blocking_reasons:
151
+ lines.append(f"> - {reason}")
152
+ lines.append("")
153
+ else:
154
+ lines.append("> **Preflight Passed** — No configured blocking policies were triggered.\n")
155
+
156
+ # Overview table
157
+ lines.append("## Overview\n")
158
+ lines.append("| Metric | Value |")
159
+ lines.append("|---|---|")
160
+ lines.append(f"| **Plan Path** | `{result.plan_path}` |")
161
+ lines.append(f"| **Environment** | `{result.environment.value.upper()}` |")
162
+ lines.append(f"| **Overall Risk Score** | **{result.overall_risk_score:.1f} / 100** |")
163
+ lines.append(f"| **Risk Category** | `{result.overall_risk_category.value.upper()}` |")
164
+ if result.highest_contributing_resource:
165
+ lines.append(f"| **Highest Risk Resource** | `{result.highest_contributing_resource}` |")
166
+ lines.append(f"| **Total Findings** | {len(result.findings)} |")
167
+ lines.append("")
168
+
169
+ # Operations breakdown
170
+ summary = result.summary
171
+ lines.append("### Planned Operations\n")
172
+ lines.append("| Create | Update | Delete | Replace | Read / No-op | Unknown Actions |")
173
+ lines.append("|:---:|:---:|:---:|:---:|:---:|:---:|")
174
+ lines.append(
175
+ f"| {summary.to_create} | {summary.to_update} | {summary.to_delete} | "
176
+ f"{summary.to_replace} | {summary.to_read} / {summary.to_noop} | {summary.unknown_actions} |"
177
+ )
178
+ lines.append("")
179
+
180
+ # Findings
181
+ if not result.findings:
182
+ lines.append("## Findings\n")
183
+ lines.append("No risk findings detected for this Terraform plan.\n")
184
+ else:
185
+ lines.append(f"## Risk Findings ({len(result.findings)})\n")
186
+ for idx, finding in enumerate(result.findings, start=1):
187
+ sev_badge = f"**{finding.severity.value.upper()}**"
188
+ lines.append(f"### {idx}. [{finding.rule_id}] {finding.rule_name} — {sev_badge}\n")
189
+ lines.append(
190
+ f"- **Resource:** `{finding.resource_address}` (`{finding.resource_type}`)"
191
+ )
192
+ lines.append(f"- **Action:** `{finding.action.value.upper()}`")
193
+ lines.append(f"- **Risk Score:** `{finding.calculated_score:.1f}`")
194
+ lines.append(f"- **What Changed:** {finding.what_changed}")
195
+ lines.append(f"- **Why It Matters:** {finding.why_it_matters}")
196
+
197
+ rf = finding.risk_factors
198
+ lines.append(
199
+ f"- **Risk Factors:** Impact: `{rf.change_impact:.1f}/5` | "
200
+ f"Security: `{rf.security_exposure:.1f}/5` | "
201
+ f"Reversibility: `{rf.reversibility:.1f}/5` | "
202
+ f"Env: `{rf.environment_criticality:.1f}/5` | "
203
+ f"Uncertainty: `{rf.uncertainty:.1f}/5`"
204
+ )
205
+ lines.append(f"- **Remediation:** {finding.remediation}\n")
206
+
207
+ lines.append("---")
208
+ lines.append(
209
+ "*Generated by [Preflight](https://github.com/preflight-engine/preflight) — Infrastructure Change Risk Analysis Engine.*"
210
+ )
211
+
212
+ return "\n".join(lines)
213
+
214
+
215
+ def get_terminal_report_str(result: AnalysisResult) -> str:
216
+ """Helper to get the terminal report as a plain string (useful for testing or writing to file)."""
217
+ buf = StringIO()
218
+ render_terminal_report(result, file=buf)
219
+ return buf.getvalue()
220
+
221
+
222
+ def _severity_to_sarif_level(severity: Severity) -> str:
223
+ if severity in (Severity.CRITICAL, Severity.HIGH):
224
+ return "error"
225
+ if severity == Severity.MODERATE:
226
+ return "warning"
227
+ return "note"
228
+
229
+
230
+ def _severity_to_security_score(severity: Severity) -> str:
231
+ if severity == Severity.CRITICAL:
232
+ return "9.0"
233
+ if severity == Severity.HIGH:
234
+ return "7.5"
235
+ if severity == Severity.MODERATE:
236
+ return "5.0"
237
+ return "2.5"
238
+
239
+
240
+ def render_sarif_report(result: AnalysisResult, indent: int = 2) -> str:
241
+ """Render analysis findings as an OASIS SARIF v2.1.0 document."""
242
+ rules_dict: dict[str, dict[str, Any]] = {}
243
+ sarif_results: list[dict[str, Any]] = []
244
+
245
+ artifact_uri = "plan.json" if result.plan_path in ("-", "<stdin>") else result.plan_path
246
+
247
+ for finding in result.findings:
248
+ if finding.rule_id not in rules_dict:
249
+ rules_dict[finding.rule_id] = {
250
+ "id": finding.rule_id,
251
+ "name": finding.rule_id.replace("-", ""),
252
+ "shortDescription": {"text": finding.rule_name},
253
+ "fullDescription": {"text": finding.why_it_matters},
254
+ "help": {
255
+ "text": finding.remediation,
256
+ "markdown": (
257
+ f"### Remediation\n\n{finding.remediation}\n\n"
258
+ f"### Why It Matters\n\n{finding.why_it_matters}"
259
+ ),
260
+ },
261
+ "defaultConfiguration": {
262
+ "level": _severity_to_sarif_level(finding.severity),
263
+ },
264
+ "properties": {
265
+ "precision": "high",
266
+ "security-severity": _severity_to_security_score(finding.severity),
267
+ "tags": ["security", "infrastructure", "terraform"],
268
+ },
269
+ }
270
+
271
+ sorted_rule_ids = sorted(rules_dict.keys())
272
+ rules_list = [rules_dict[rid] for rid in sorted_rule_ids]
273
+ rule_index_map = {rid: idx for idx, rid in enumerate(sorted_rule_ids)}
274
+
275
+ for finding in result.findings:
276
+ sarif_results.append(
277
+ {
278
+ "ruleId": finding.rule_id,
279
+ "ruleIndex": rule_index_map[finding.rule_id],
280
+ "level": _severity_to_sarif_level(finding.severity),
281
+ "message": {
282
+ "text": (
283
+ f"{finding.what_changed}\n\n"
284
+ f"Why it matters: {finding.why_it_matters}\n"
285
+ f"Remediation: {finding.remediation}"
286
+ ),
287
+ },
288
+ "locations": [
289
+ {
290
+ "physicalLocation": {
291
+ "artifactLocation": {
292
+ "uri": artifact_uri,
293
+ "uriBaseId": "%SRCROOT%",
294
+ },
295
+ "region": {
296
+ "startLine": 1,
297
+ "startColumn": 1,
298
+ },
299
+ },
300
+ "logicalLocations": [
301
+ {
302
+ "name": finding.resource_address,
303
+ "kind": "resource",
304
+ "fullyQualifiedName": f"{finding.resource_type}.{finding.resource_address}",
305
+ }
306
+ ],
307
+ }
308
+ ],
309
+ "properties": {
310
+ "resourceAddress": finding.resource_address,
311
+ "resourceType": finding.resource_type,
312
+ "action": finding.action.value,
313
+ "riskScore": finding.calculated_score,
314
+ "severity": finding.severity.value,
315
+ },
316
+ }
317
+ )
318
+
319
+ sarif_doc = {
320
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
321
+ "version": "2.1.0",
322
+ "runs": [
323
+ {
324
+ "tool": {
325
+ "driver": {
326
+ "name": "Preflight",
327
+ "semanticVersion": __version__,
328
+ "informationUri": "https://github.com/preflight-engine/preflight",
329
+ "rules": rules_list,
330
+ }
331
+ },
332
+ "results": sarif_results,
333
+ }
334
+ ],
335
+ }
336
+
337
+ return json.dumps(sarif_doc, indent=indent)
338
+
339
+
340
+ def render_pr_comment(result: AnalysisResult) -> str:
341
+ """Render a collapsible, sticky markdown comment for GitHub pull requests."""
342
+ cat_badge = f"**{result.overall_risk_category.value.upper()}**"
343
+ policy_badge = "**BLOCKED**" if result.policy_evaluation.blocked else "**PASSED**"
344
+
345
+ lines: list[str] = [
346
+ PR_COMMENT_MARKER,
347
+ "## Preflight Infrastructure Risk Analysis\n",
348
+ "| Status | Risk Score | Environment | Policy Gate |",
349
+ "| :---: | :---: | :---: | :---: |",
350
+ f"| {cat_badge} | **{result.overall_risk_score:.1f} / 100** | `{result.environment.value.upper()}` | {policy_badge} |\n",
351
+ ]
352
+
353
+ if result.policy_evaluation.blocked:
354
+ lines.append("> **Deployment Blocked by Policy:**")
355
+ for reason in result.policy_evaluation.blocking_reasons:
356
+ lines.append(f"> - {reason}")
357
+ lines.append("")
358
+
359
+ summary = result.summary
360
+ lines.append("### Planned Resource Operations\n")
361
+ lines.append("| Create | Update | Delete | Replace | Read / No-op | Unknown Actions |")
362
+ lines.append("| :---: | :---: | :---: | :---: | :---: | :---: |")
363
+ lines.append(
364
+ f"| {summary.to_create} | {summary.to_update} | {summary.to_delete} | "
365
+ f"{summary.to_replace} | {summary.to_read} / {summary.to_noop} | {summary.unknown_actions} |\n"
366
+ )
367
+
368
+ if not result.findings:
369
+ lines.append("### Findings\n")
370
+ lines.append(
371
+ "> **No risk findings detected.** All planned changes are within acceptable thresholds.\n"
372
+ )
373
+ else:
374
+ lines.append(f"### Risk Findings ({len(result.findings)})\n")
375
+ for idx, finding in enumerate(result.findings, start=1):
376
+ open_attr = " open" if idx <= 3 or finding.severity == Severity.CRITICAL else ""
377
+ lines.append(f"<details{open_attr}>")
378
+ lines.append(
379
+ f"<summary><b>[{finding.rule_id}] {finding.rule_name} — {finding.severity.value.upper()} (Score: {finding.calculated_score:.1f})</b></summary>\n"
380
+ )
381
+ lines.append(
382
+ f"- **Resource:** `{finding.resource_address}` (`{finding.resource_type}`)"
383
+ )
384
+ lines.append(f"- **Action:** `{finding.action.value.upper()}`")
385
+ lines.append(f"- **What Changed:** {finding.what_changed}")
386
+ lines.append(f"- **Why It Matters:** {finding.why_it_matters}")
387
+
388
+ rf = finding.risk_factors
389
+ lines.append(
390
+ f"- **Risk Factors:** Impact: `{rf.change_impact:.1f}/5` | "
391
+ f"Security: `{rf.security_exposure:.1f}/5` | "
392
+ f"Reversibility: `{rf.reversibility:.1f}/5` | "
393
+ f"Env: `{rf.environment_criticality:.1f}/5` | "
394
+ f"Uncertainty: `{rf.uncertainty:.1f}/5`"
395
+ )
396
+ lines.append(f"- **Remediation:** {finding.remediation}\n")
397
+ lines.append("</details>\n")
398
+
399
+ lines.append("---")
400
+ lines.append(
401
+ "*Updated automatically by [Preflight](https://github.com/preflight-engine/preflight) — Infrastructure Change Risk Analysis Engine.*"
402
+ )
403
+
404
+ return "\n".join(lines)
@@ -0,0 +1,85 @@
1
+ """Rule packs and registry for Preflight."""
2
+
3
+ from preflight.rules.aws import (
4
+ AwsDatabaseDeletionReplacementRule,
5
+ AwsIamWildcardRule,
6
+ AwsPublicAdminIngressRule,
7
+ AwsS3PublicAccessBlockRule,
8
+ )
9
+ from preflight.rules.azure import (
10
+ AzureDatabaseDeletionReplacementRule,
11
+ AzureKeyVaultPurgeProtectionRule,
12
+ AzureNsgPublicAdminIngressRule,
13
+ AzureStoragePublicAccessRule,
14
+ )
15
+ from preflight.rules.base import Rule, RuleContext
16
+ from preflight.rules.gcp import (
17
+ GcpFirewallPublicAdminIngressRule,
18
+ GcpSqlDeletionProtectionRule,
19
+ GcpStoragePublicAccessRule,
20
+ )
21
+ from preflight.rules.generic import (
22
+ GenericDestructiveChangeRule,
23
+ GenericResourceDeletionRule,
24
+ GenericResourceReplacementRule,
25
+ GenericUnknownActionRule,
26
+ GenericUnknownValuesRule,
27
+ )
28
+ from preflight.rules.kubernetes import (
29
+ KubernetesClusterAdminBindingRule,
30
+ KubernetesWorkloadSecurityRule,
31
+ )
32
+
33
+
34
+ def get_default_rules() -> list[Rule]:
35
+ """Return an instantiated list of all standard detection rules."""
36
+ return [
37
+ # Provider-neutral rules
38
+ GenericResourceDeletionRule(),
39
+ GenericResourceReplacementRule(),
40
+ GenericUnknownActionRule(),
41
+ GenericUnknownValuesRule(),
42
+ GenericDestructiveChangeRule(),
43
+ # AWS rules
44
+ AwsPublicAdminIngressRule(),
45
+ AwsS3PublicAccessBlockRule(),
46
+ AwsIamWildcardRule(),
47
+ AwsDatabaseDeletionReplacementRule(),
48
+ # Azure rules
49
+ AzureNsgPublicAdminIngressRule(),
50
+ AzureStoragePublicAccessRule(),
51
+ AzureKeyVaultPurgeProtectionRule(),
52
+ AzureDatabaseDeletionReplacementRule(),
53
+ # GCP rules
54
+ GcpFirewallPublicAdminIngressRule(),
55
+ GcpStoragePublicAccessRule(),
56
+ GcpSqlDeletionProtectionRule(),
57
+ # Kubernetes rules
58
+ KubernetesWorkloadSecurityRule(),
59
+ KubernetesClusterAdminBindingRule(),
60
+ ]
61
+
62
+
63
+ __all__ = [
64
+ "Rule",
65
+ "RuleContext",
66
+ "get_default_rules",
67
+ "GenericResourceDeletionRule",
68
+ "GenericResourceReplacementRule",
69
+ "GenericUnknownActionRule",
70
+ "GenericUnknownValuesRule",
71
+ "GenericDestructiveChangeRule",
72
+ "AwsPublicAdminIngressRule",
73
+ "AwsS3PublicAccessBlockRule",
74
+ "AwsIamWildcardRule",
75
+ "AwsDatabaseDeletionReplacementRule",
76
+ "AzureNsgPublicAdminIngressRule",
77
+ "AzureStoragePublicAccessRule",
78
+ "AzureKeyVaultPurgeProtectionRule",
79
+ "AzureDatabaseDeletionReplacementRule",
80
+ "GcpFirewallPublicAdminIngressRule",
81
+ "GcpStoragePublicAccessRule",
82
+ "GcpSqlDeletionProtectionRule",
83
+ "KubernetesWorkloadSecurityRule",
84
+ "KubernetesClusterAdminBindingRule",
85
+ ]