flareguard 0.1.0

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 (64) hide show
  1. package/Cargo.lock +2962 -0
  2. package/Cargo.toml +66 -0
  3. package/LICENSE +21 -0
  4. package/README.md +115 -0
  5. package/bin/flareguard.js +78 -0
  6. package/package.json +50 -0
  7. package/scripts/postinstall.mjs +83 -0
  8. package/src/bindings/ast_scanner.rs +950 -0
  9. package/src/bindings/cli.rs +49 -0
  10. package/src/bindings/jsonc.rs +166 -0
  11. package/src/bindings/mod.rs +7 -0
  12. package/src/bindings/reporter.rs +328 -0
  13. package/src/bindings/types.rs +129 -0
  14. package/src/bindings/validator.rs +227 -0
  15. package/src/bindings/wrangler.rs +647 -0
  16. package/src/cli.rs +77 -0
  17. package/src/lib.rs +7 -0
  18. package/src/main.rs +451 -0
  19. package/src/origin/cli.rs +92 -0
  20. package/src/origin/cloudflare.rs +170 -0
  21. package/src/origin/confidence.rs +320 -0
  22. package/src/origin/crtsh.rs +144 -0
  23. package/src/origin/dns.rs +177 -0
  24. package/src/origin/enumerator.rs +271 -0
  25. package/src/origin/error.rs +19 -0
  26. package/src/origin/mock.rs +320 -0
  27. package/src/origin/mod.rs +19 -0
  28. package/src/origin/models.rs +167 -0
  29. package/src/origin/prober.rs +260 -0
  30. package/src/origin/remediation.rs +73 -0
  31. package/src/origin/report.rs +625 -0
  32. package/src/origin/scanner.rs +217 -0
  33. package/src/secrets/cli.rs +94 -0
  34. package/src/secrets/env_parser.rs +464 -0
  35. package/src/secrets/ignore.rs +139 -0
  36. package/src/secrets/mod.rs +14 -0
  37. package/src/secrets/report/json_format.rs +97 -0
  38. package/src/secrets/report/mod.rs +48 -0
  39. package/src/secrets/report/sarif.rs +225 -0
  40. package/src/secrets/report/text.rs +105 -0
  41. package/src/secrets/rules/builtin.rs +280 -0
  42. package/src/secrets/rules/entropy.rs +66 -0
  43. package/src/secrets/rules/mod.rs +7 -0
  44. package/src/secrets/rules/types.rs +183 -0
  45. package/src/secrets/scanner.rs +444 -0
  46. package/src/zone/cli.rs +111 -0
  47. package/src/zone/client/cf_client.rs +405 -0
  48. package/src/zone/client/mod.rs +5 -0
  49. package/src/zone/client/provider.rs +12 -0
  50. package/src/zone/mock_data.rs +481 -0
  51. package/src/zone/mod.rs +125 -0
  52. package/src/zone/models/audit.rs +194 -0
  53. package/src/zone/models/cloudflare.rs +252 -0
  54. package/src/zone/models/mod.rs +7 -0
  55. package/src/zone/models/sarif.rs +89 -0
  56. package/src/zone/reporters/html_rep.rs +345 -0
  57. package/src/zone/reporters/json_rep.rs +7 -0
  58. package/src/zone/reporters/mod.rs +9 -0
  59. package/src/zone/reporters/sarif_rep.rs +100 -0
  60. package/src/zone/reporters/terminal.rs +322 -0
  61. package/src/zone/rules/definitions.rs +201 -0
  62. package/src/zone/rules/evaluator.rs +484 -0
  63. package/src/zone/rules/mod.rs +5 -0
  64. package/src/zone/scoring.rs +104 -0
@@ -0,0 +1,345 @@
1
+ use crate::zone::models::{AggregateAuditReport, RiskLevel};
2
+
3
+ pub fn render_html(report: &AggregateAuditReport) -> String {
4
+ let mut html = String::with_capacity(16384);
5
+
6
+ let grade_badge_class = match report.overall_grade.as_str() {
7
+ "A+" | "A" => "grade-a",
8
+ "B" => "grade-b",
9
+ "C" => "grade-c",
10
+ "D" => "grade-d",
11
+ _ => "grade-f",
12
+ };
13
+
14
+ let score_color = if report.average_score >= 90.0 {
15
+ "#10b981"
16
+ } else if report.average_score >= 80.0 {
17
+ "#059669"
18
+ } else if report.average_score >= 70.0 {
19
+ "#f59e0b"
20
+ } else {
21
+ "#ef4444"
22
+ };
23
+
24
+ html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
25
+ html.push_str("<meta charset=\"UTF-8\">\n");
26
+ html.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
27
+ html.push_str("<title>Cloudflare Zone Security Audit Report</title>\n");
28
+ html.push_str("<style>\n");
29
+ html.push_str(r#"
30
+ :root {
31
+ --bg-primary: #0f172a;
32
+ --bg-card: #1e293b;
33
+ --bg-card-hover: #334155;
34
+ --text-primary: #f8fafc;
35
+ --text-secondary: #94a3b8;
36
+ --border-color: #334155;
37
+ --accent: #38bdf8;
38
+ --critical: #ef4444;
39
+ --high: #f97316;
40
+ --medium: #eab308;
41
+ --low: #38bdf8;
42
+ --info: #94a3b8;
43
+ --pass: #10b981;
44
+ }
45
+
46
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
47
+ body { background-color: var(--bg-primary); color: var(--text-primary); padding: 2rem; line-height: 1.5; }
48
+ .container { max-width: 1200px; margin: 0 auto; }
49
+
50
+ header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 1.5rem; margin-bottom: 2rem; }
51
+ .header-title h1 { font-size: 1.8rem; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 0.5rem; }
52
+ .header-title p { color: var(--text-secondary); font-size: 0.95rem; margin-top: 0.25rem; }
53
+ .timestamp { font-size: 0.85rem; color: var(--text-secondary); }
54
+
55
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.25rem; margin-bottom: 2rem; }
56
+ .card { background-color: var(--bg-card); border: 1px solid var(--border-color); border-radius: 0.75rem; padding: 1.25rem; }
57
+ .card-label { font-size: 0.85rem; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; letter-spacing: 0.05em; }
58
+ .card-val { font-size: 2rem; font-weight: 800; margin-top: 0.5rem; }
59
+
60
+ .grade-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 0.5rem; font-weight: 800; font-size: 1.5rem; }
61
+ .grade-a { background-color: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid #10b981; }
62
+ .grade-b { background-color: rgba(5, 150, 105, 0.2); color: #34d399; border: 1px solid #34d399; }
63
+ .grade-c { background-color: rgba(234, 179, 8, 0.2); color: #eab308; border: 1px solid #eab308; }
64
+ .grade-d { background-color: rgba(249, 115, 22, 0.2); color: #f97316; border: 1px solid #f97316; }
65
+ .grade-f { background-color: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid #ef4444; }
66
+
67
+ .severity-pill-group { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem; }
68
+ .pill { padding: 0.2rem 0.6rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 700; }
69
+ .pill-crit { background: rgba(239, 68, 68, 0.2); color: var(--critical); border: 1px solid var(--critical); }
70
+ .pill-high { background: rgba(249, 115, 22, 0.2); color: var(--high); border: 1px solid var(--high); }
71
+ .pill-med { background: rgba(234, 179, 8, 0.2); color: var(--medium); border: 1px solid var(--medium); }
72
+ .pill-low { background: rgba(56, 189, 248, 0.2); color: var(--low); border: 1px solid var(--low); }
73
+ .pill-info { background: rgba(148, 163, 184, 0.2); color: var(--info); border: 1px solid var(--info); }
74
+ .pill-pass { background: rgba(16, 185, 129, 0.2); color: var(--pass); border: 1px solid var(--pass); }
75
+
76
+ section { margin-bottom: 2.5rem; }
77
+ h2 { font-size: 1.3rem; margin-bottom: 1rem; color: #fff; border-left: 4px solid var(--accent); padding-left: 0.75rem; }
78
+
79
+ table { width: 100%; border-collapse: collapse; background-color: var(--bg-card); border-radius: 0.75rem; overflow: hidden; border: 1px solid var(--border-color); }
80
+ th, td { padding: 0.85rem 1rem; text-align: left; border-bottom: 1px solid var(--border-color); font-size: 0.9rem; }
81
+ th { background-color: rgba(15, 23, 42, 0.6); color: var(--text-secondary); font-weight: 600; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; }
82
+ tr:last-child td { border-bottom: none; }
83
+ tr:hover { background-color: rgba(51, 65, 85, 0.3); }
84
+
85
+ .zone-accordion { margin-bottom: 1rem; border: 1px solid var(--border-color); border-radius: 0.75rem; background-color: var(--bg-card); overflow: hidden; }
86
+ .zone-header { padding: 1rem 1.25rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; }
87
+ .zone-header:hover { background-color: var(--bg-card-hover); }
88
+ .zone-info { display: flex; align-items: center; gap: 1rem; }
89
+ .zone-name { font-weight: 700; font-size: 1.1rem; }
90
+ .zone-meta { color: var(--text-secondary); font-size: 0.85rem; }
91
+
92
+ .findings-list { padding: 1.25rem; border-top: 1px solid var(--border-color); background-color: rgba(15, 23, 42, 0.4); }
93
+ .finding-item { background-color: var(--bg-card); border: 1px solid var(--border-color); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
94
+ .finding-item:last-child { margin-bottom: 0; }
95
+ .finding-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; }
96
+ .finding-title { font-weight: 700; font-size: 0.95rem; color: #fff; }
97
+ .finding-desc { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 0.75rem; }
98
+ .finding-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; background-color: rgba(15, 23, 42, 0.5); padding: 0.75rem; border-radius: 0.375rem; font-size: 0.85rem; margin-bottom: 0.75rem; }
99
+ .remediation-box { background-color: rgba(56, 189, 248, 0.1); border-left: 3px solid var(--accent); padding: 0.75rem; border-radius: 0.25rem; font-size: 0.85rem; }
100
+ .remediation-label { font-weight: 700; color: var(--accent); margin-bottom: 0.25rem; }
101
+
102
+ footer { margin-top: 3rem; text-align: center; color: var(--text-secondary); font-size: 0.8rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem; }
103
+ "#);
104
+ html.push_str("</style>\n</head>\n<body>\n<div class=\"container\">\n");
105
+
106
+ // Header
107
+ html.push_str("<header>\n<div class=\"header-title\">\n");
108
+ html.push_str("<h1>🛡️ Cloudflare Zone Security Auditor</h1>\n");
109
+ html.push_str("<p>Automated Multi-Zone Security Posture & Compliance Report</p>\n");
110
+ html.push_str("</div>\n<div class=\"timestamp\">\n");
111
+ html.push_str(&format!(
112
+ "Generated: {}<br>",
113
+ report.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
114
+ ));
115
+ if let Some(ref acc) = report.account_id {
116
+ html.push_str(&format!("Account: <code>{}</code>\n", acc));
117
+ }
118
+ html.push_str("</div>\n</header>\n");
119
+
120
+ // Summary Cards
121
+ html.push_str("<div class=\"summary-grid\">\n");
122
+
123
+ // Card 1: Score
124
+ html.push_str("<div class=\"card\">\n<div class=\"card-label\">Overall Security Score</div>\n");
125
+ html.push_str(&format!(
126
+ "<div class=\"card-val\" style=\"color: {};\">{:.1} <span style=\"font-size: 1rem; color: var(--text-secondary);\">/ 100</span></div>\n",
127
+ score_color, report.average_score
128
+ ));
129
+ html.push_str("</div>\n");
130
+
131
+ // Card 2: Health Grade
132
+ html.push_str("<div class=\"card\">\n<div class=\"card-label\">Overall Health Grade</div>\n");
133
+ html.push_str(&format!(
134
+ "<div style=\"margin-top: 0.5rem;\"><span class=\"grade-badge {}\">{}</span></div>\n",
135
+ grade_badge_class, report.overall_grade
136
+ ));
137
+ html.push_str("</div>\n");
138
+
139
+ // Card 3: Total Zones
140
+ html.push_str("<div class=\"card\">\n<div class=\"card-label\">Zones Audited</div>\n");
141
+ html.push_str(&format!(
142
+ "<div class=\"card-val\">{}</div>\n",
143
+ report.total_zones
144
+ ));
145
+ html.push_str("</div>\n");
146
+
147
+ // Card 4: Findings Breakdown
148
+ html.push_str("<div class=\"card\">\n<div class=\"card-label\">Total Findings</div>\n");
149
+ html.push_str(&format!(
150
+ "<div class=\"card-val\">{}</div>\n",
151
+ report.total_findings.total
152
+ ));
153
+ html.push_str("<div class=\"severity-pill-group\">\n");
154
+ if report.total_findings.critical > 0 {
155
+ html.push_str(&format!(
156
+ "<span class=\"pill pill-crit\">{} CRITICAL</span>\n",
157
+ report.total_findings.critical
158
+ ));
159
+ }
160
+ if report.total_findings.high > 0 {
161
+ html.push_str(&format!(
162
+ "<span class=\"pill pill-high\">{} HIGH</span>\n",
163
+ report.total_findings.high
164
+ ));
165
+ }
166
+ if report.total_findings.medium > 0 {
167
+ html.push_str(&format!(
168
+ "<span class=\"pill pill-med\">{} MED</span>\n",
169
+ report.total_findings.medium
170
+ ));
171
+ }
172
+ if report.total_findings.low > 0 {
173
+ html.push_str(&format!(
174
+ "<span class=\"pill pill-low\">{} LOW</span>\n",
175
+ report.total_findings.low
176
+ ));
177
+ }
178
+ html.push_str("</div>\n</div>\n</div>\n");
179
+
180
+ // Zone Posture Table
181
+ html.push_str("<section>\n<h2>🌐 Zone Security Posture Overview</h2>\n");
182
+ html.push_str("<table>\n<thead>\n<tr>\n");
183
+ html.push_str("<th>Zone Name</th><th>Plan</th><th>SSL Mode</th><th>Min TLS</th><th>Always HTTPS</th><th>HSTS</th><th>WAF</th><th>DNSSEC</th><th>Score</th><th>Grade</th>\n");
184
+ html.push_str("</tr>\n</thead>\n<tbody>\n");
185
+
186
+ for z in &report.zone_reports {
187
+ let ssl_pill = match z.settings_summary.ssl_mode.to_lowercase().as_str() {
188
+ "strict" => "<span class=\"pill pill-pass\">strict</span>",
189
+ "full" => "<span class=\"pill pill-med\">full</span>",
190
+ _ => "<span class=\"pill pill-crit\">flexible / off</span>",
191
+ };
192
+
193
+ let tls_pill = if z.settings_summary.min_tls.contains("1.3")
194
+ || z.settings_summary.min_tls.contains("1.2")
195
+ {
196
+ format!(
197
+ "<span class=\"pill pill-pass\">{}</span>",
198
+ z.settings_summary.min_tls
199
+ )
200
+ } else {
201
+ format!(
202
+ "<span class=\"pill pill-crit\">{}</span>",
203
+ z.settings_summary.min_tls
204
+ )
205
+ };
206
+
207
+ let bool_pill = |val: &str| {
208
+ if val.eq_ignore_ascii_case("enabled") || val.eq_ignore_ascii_case("active") {
209
+ "<span class=\"pill pill-pass\">Enabled</span>".to_string()
210
+ } else {
211
+ "<span class=\"pill pill-crit\">Disabled</span>".to_string()
212
+ }
213
+ };
214
+
215
+ let dnssec_pill = match z.settings_summary.dnssec_status.to_lowercase().as_str() {
216
+ "active" => "<span class=\"pill pill-pass\">Active</span>",
217
+ "pending" => "<span class=\"pill pill-med\">Pending</span>",
218
+ _ => "<span class=\"pill pill-crit\">Disabled</span>",
219
+ };
220
+
221
+ let zone_grade_class = match z.grade.as_str() {
222
+ "A+" | "A" => "pill-pass",
223
+ "B" => "pill-low",
224
+ "C" => "pill-med",
225
+ "D" => "pill-high",
226
+ _ => "pill-crit",
227
+ };
228
+
229
+ html.push_str("<tr>\n");
230
+ html.push_str(&format!("<td><strong>{}</strong></td>\n", z.zone_name));
231
+ html.push_str(&format!("<td>{}</td>\n", z.plan_name));
232
+ html.push_str(&format!("<td>{}</td>\n", ssl_pill));
233
+ html.push_str(&format!("<td>{}</td>\n", tls_pill));
234
+ html.push_str(&format!(
235
+ "<td>{}</td>\n",
236
+ bool_pill(&z.settings_summary.always_https)
237
+ ));
238
+ html.push_str(&format!(
239
+ "<td>{}</td>\n",
240
+ bool_pill(&z.settings_summary.hsts_status)
241
+ ));
242
+ html.push_str(&format!(
243
+ "<td>{}</td>\n",
244
+ bool_pill(&z.settings_summary.waf_status)
245
+ ));
246
+ html.push_str(&format!("<td>{}</td>\n", dnssec_pill));
247
+ html.push_str(&format!("<td><strong>{}/100</strong></td>\n", z.score));
248
+ html.push_str(&format!(
249
+ "<td><span class=\"pill {}\">{}</span></td>\n",
250
+ zone_grade_class, z.grade
251
+ ));
252
+ html.push_str("</tr>\n");
253
+ }
254
+ html.push_str("</tbody>\n</table>\n</section>\n");
255
+
256
+ // Detailed Findings Accordion
257
+ html.push_str("<section>\n<h2>🔍 Detailed Zone Findings & Remediation</h2>\n");
258
+
259
+ for z in &report.zone_reports {
260
+ let finding_count = z.findings.len();
261
+ html.push_str("<div class=\"zone-accordion\">\n");
262
+ html.push_str("<div class=\"zone-header\">\n");
263
+ html.push_str("<div class=\"zone-info\">\n");
264
+ html.push_str(&format!(
265
+ "<span class=\"zone-name\">{}</span>\n",
266
+ z.zone_name
267
+ ));
268
+ html.push_str(&format!(
269
+ "<span class=\"zone-meta\">Plan: {} | ID: {}</span>\n",
270
+ z.plan_name, z.zone_id
271
+ ));
272
+ html.push_str("</div>\n<div>\n");
273
+ html.push_str(&format!(
274
+ "<span class=\"pill {}\">Score: {} ({})</span> ",
275
+ match z.grade.as_str() {
276
+ "A+" | "A" => "pill-pass",
277
+ "B" => "pill-low",
278
+ "C" => "pill-med",
279
+ "D" => "pill-high",
280
+ _ => "pill-crit",
281
+ },
282
+ z.score,
283
+ z.grade
284
+ ));
285
+ html.push_str(&format!(
286
+ "<span class=\"zone-meta\">{} Findings</span>\n",
287
+ finding_count
288
+ ));
289
+ html.push_str("</div>\n</div>\n");
290
+
291
+ if !z.findings.is_empty() {
292
+ html.push_str("<div class=\"findings-list\">\n");
293
+ for f in &z.findings {
294
+ let sev_class = match f.risk_level {
295
+ RiskLevel::Critical => "pill-crit",
296
+ RiskLevel::High => "pill-high",
297
+ RiskLevel::Medium => "pill-med",
298
+ RiskLevel::Low => "pill-low",
299
+ RiskLevel::Info => "pill-info",
300
+ };
301
+
302
+ html.push_str("<div class=\"finding-item\">\n");
303
+ html.push_str("<div class=\"finding-head\">\n");
304
+ html.push_str(&format!("<div><span class=\"finding-title\">{}</span> <span class=\"zone-meta\">[{}]</span></div>\n", f.title, f.rule_id));
305
+ html.push_str(&format!(
306
+ "<span class=\"pill {}\">{}</span>\n",
307
+ sev_class, f.risk_level
308
+ ));
309
+ html.push_str("</div>\n");
310
+ html.push_str(&format!(
311
+ "<div class=\"finding-desc\">{}</div>\n",
312
+ f.description
313
+ ));
314
+ html.push_str("<div class=\"finding-grid\">\n");
315
+ html.push_str(&format!(
316
+ "<div><strong>Current:</strong> {}</div>\n",
317
+ f.actual_value
318
+ ));
319
+ html.push_str(&format!(
320
+ "<div><strong>Recommended:</strong> {}</div>\n",
321
+ f.expected_value
322
+ ));
323
+ html.push_str("</div>\n");
324
+ html.push_str("<div class=\"remediation-box\">\n");
325
+ html.push_str("<div class=\"remediation-label\">🛠️ Remediation Guidance</div>\n");
326
+ html.push_str(&format!("<div>{}</div>\n", f.remediation));
327
+ if let Some(ref doc) = f.doc_url {
328
+ html.push_str(&format!("<div style=\"margin-top: 0.35rem;\"><a href=\"{}\" target=\"_blank\" style=\"color: var(--accent); font-size: 0.8rem;\">Cloudflare Documentation &rarr;</a></div>\n", doc));
329
+ }
330
+ html.push_str("</div>\n</div>\n");
331
+ }
332
+ html.push_str("</div>\n");
333
+ }
334
+ html.push_str("</div>\n");
335
+ }
336
+
337
+ html.push_str("</section>\n");
338
+
339
+ // Footer
340
+ html.push_str("<footer>\n");
341
+ html.push_str("Generated by <strong>cf-zone-auditor</strong> • Cloudflare Zone Security Auditor & Compliance Gate\n");
342
+ html.push_str("</footer>\n</div>\n</body>\n</html>\n");
343
+
344
+ html
345
+ }
@@ -0,0 +1,7 @@
1
+ use crate::zone::models::AggregateAuditReport;
2
+ use anyhow::Result;
3
+
4
+ pub fn render_json(report: &AggregateAuditReport) -> Result<String> {
5
+ let json_str = serde_json::to_string_pretty(report)?;
6
+ Ok(json_str)
7
+ }
@@ -0,0 +1,9 @@
1
+ pub mod html_rep;
2
+ pub mod json_rep;
3
+ pub mod sarif_rep;
4
+ pub mod terminal;
5
+
6
+ pub use html_rep::render_html;
7
+ pub use json_rep::render_json;
8
+ pub use sarif_rep::render_sarif;
9
+ pub use terminal::render_terminal;
@@ -0,0 +1,100 @@
1
+ use crate::zone::models::{
2
+ AggregateAuditReport, RiskLevel, SarifArtifactLocation, SarifDriver, SarifLocation,
3
+ SarifMessage, SarifPhysicalLocation, SarifProperties, SarifReport, SarifResult,
4
+ SarifRuleConfiguration, SarifRuleDescriptor, SarifRun, SarifTool,
5
+ };
6
+ use crate::zone::rules::definitions::ALL_RULES;
7
+ use anyhow::Result;
8
+
9
+ pub fn render_sarif(report: &AggregateAuditReport) -> Result<String> {
10
+ let mut sarif_rules = Vec::new();
11
+
12
+ for def in ALL_RULES {
13
+ let level = match def.default_severity {
14
+ RiskLevel::Critical | RiskLevel::High => "error",
15
+ RiskLevel::Medium => "warning",
16
+ RiskLevel::Low | RiskLevel::Info => "note",
17
+ };
18
+
19
+ let sec_severity = match def.default_severity {
20
+ RiskLevel::Critical => "9.0",
21
+ RiskLevel::High => "7.5",
22
+ RiskLevel::Medium => "5.0",
23
+ RiskLevel::Low => "2.5",
24
+ RiskLevel::Info => "1.0",
25
+ };
26
+
27
+ sarif_rules.push(SarifRuleDescriptor {
28
+ id: def.id.to_string(),
29
+ name: def.name.to_string(),
30
+ short_description: SarifMessage {
31
+ text: def.title.to_string(),
32
+ },
33
+ full_description: SarifMessage {
34
+ text: format!("{}. Remediation: {}", def.description, def.remediation),
35
+ },
36
+ default_configuration: SarifRuleConfiguration {
37
+ level: level.to_string(),
38
+ },
39
+ help_uri: Some(def.doc_url.to_string()),
40
+ properties: Some(SarifProperties {
41
+ tags: vec![
42
+ "security".to_string(),
43
+ "cloudflare".to_string(),
44
+ format!("{:?}", def.category).to_lowercase(),
45
+ ],
46
+ precision: "high".to_string(),
47
+ security_severity: Some(sec_severity.to_string()),
48
+ }),
49
+ });
50
+ }
51
+
52
+ let mut results = Vec::new();
53
+
54
+ for z in &report.zone_reports {
55
+ for f in &z.findings {
56
+ let level = match f.risk_level {
57
+ RiskLevel::Critical | RiskLevel::High => "error",
58
+ RiskLevel::Medium => "warning",
59
+ RiskLevel::Low | RiskLevel::Info => "note",
60
+ };
61
+
62
+ let message_text = format!(
63
+ "{}: {}. Actual: '{}', Expected: '{}'. Remediation: {}",
64
+ f.title, f.description, f.actual_value, f.expected_value, f.remediation
65
+ );
66
+
67
+ results.push(SarifResult {
68
+ rule_id: f.rule_id.clone(),
69
+ level: level.to_string(),
70
+ message: SarifMessage { text: message_text },
71
+ locations: vec![SarifLocation {
72
+ physical_location: SarifPhysicalLocation {
73
+ artifact_location: SarifArtifactLocation {
74
+ uri: format!("zone:{}", z.zone_name),
75
+ },
76
+ },
77
+ }],
78
+ });
79
+ }
80
+ }
81
+
82
+ let sarif = SarifReport {
83
+ schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
84
+ version: "2.1.0".to_string(),
85
+ runs: vec![SarifRun {
86
+ tool: SarifTool {
87
+ driver: SarifDriver {
88
+ name: "cf-zone-auditor".to_string(),
89
+ version: env!("CARGO_PKG_VERSION").to_string(),
90
+ information_uri: "https://github.com/cloudflare-security-report/cf-zone-auditor".to_string(),
91
+ rules: sarif_rules,
92
+ },
93
+ },
94
+ results,
95
+ }],
96
+ };
97
+
98
+ let json_str = serde_json::to_string_pretty(&sarif)?;
99
+ Ok(json_str)
100
+ }