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.
- package/Cargo.lock +2962 -0
- package/Cargo.toml +66 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/flareguard.js +78 -0
- package/package.json +50 -0
- package/scripts/postinstall.mjs +83 -0
- package/src/bindings/ast_scanner.rs +950 -0
- package/src/bindings/cli.rs +49 -0
- package/src/bindings/jsonc.rs +166 -0
- package/src/bindings/mod.rs +7 -0
- package/src/bindings/reporter.rs +328 -0
- package/src/bindings/types.rs +129 -0
- package/src/bindings/validator.rs +227 -0
- package/src/bindings/wrangler.rs +647 -0
- package/src/cli.rs +77 -0
- package/src/lib.rs +7 -0
- package/src/main.rs +451 -0
- package/src/origin/cli.rs +92 -0
- package/src/origin/cloudflare.rs +170 -0
- package/src/origin/confidence.rs +320 -0
- package/src/origin/crtsh.rs +144 -0
- package/src/origin/dns.rs +177 -0
- package/src/origin/enumerator.rs +271 -0
- package/src/origin/error.rs +19 -0
- package/src/origin/mock.rs +320 -0
- package/src/origin/mod.rs +19 -0
- package/src/origin/models.rs +167 -0
- package/src/origin/prober.rs +260 -0
- package/src/origin/remediation.rs +73 -0
- package/src/origin/report.rs +625 -0
- package/src/origin/scanner.rs +217 -0
- package/src/secrets/cli.rs +94 -0
- package/src/secrets/env_parser.rs +464 -0
- package/src/secrets/ignore.rs +139 -0
- package/src/secrets/mod.rs +14 -0
- package/src/secrets/report/json_format.rs +97 -0
- package/src/secrets/report/mod.rs +48 -0
- package/src/secrets/report/sarif.rs +225 -0
- package/src/secrets/report/text.rs +105 -0
- package/src/secrets/rules/builtin.rs +280 -0
- package/src/secrets/rules/entropy.rs +66 -0
- package/src/secrets/rules/mod.rs +7 -0
- package/src/secrets/rules/types.rs +183 -0
- package/src/secrets/scanner.rs +444 -0
- package/src/zone/cli.rs +111 -0
- package/src/zone/client/cf_client.rs +405 -0
- package/src/zone/client/mod.rs +5 -0
- package/src/zone/client/provider.rs +12 -0
- package/src/zone/mock_data.rs +481 -0
- package/src/zone/mod.rs +125 -0
- package/src/zone/models/audit.rs +194 -0
- package/src/zone/models/cloudflare.rs +252 -0
- package/src/zone/models/mod.rs +7 -0
- package/src/zone/models/sarif.rs +89 -0
- package/src/zone/reporters/html_rep.rs +345 -0
- package/src/zone/reporters/json_rep.rs +7 -0
- package/src/zone/reporters/mod.rs +9 -0
- package/src/zone/reporters/sarif_rep.rs +100 -0
- package/src/zone/reporters/terminal.rs +322 -0
- package/src/zone/rules/definitions.rs +201 -0
- package/src/zone/rules/evaluator.rs +484 -0
- package/src/zone/rules/mod.rs +5 -0
- package/src/zone/scoring.rs +104 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
use crate::zone::models::{
|
|
2
|
+
AuditFinding, RiskLevel, ZoneAuditData, ZoneAuditReport, ZoneSettingsSummary,
|
|
3
|
+
};
|
|
4
|
+
use crate::zone::rules::definitions::get_rule_by_id;
|
|
5
|
+
use crate::zone::scoring::{calculate_grade, calculate_zone_score};
|
|
6
|
+
|
|
7
|
+
/// Audits a single zone against all security rules and returns a structured ZoneAuditReport
|
|
8
|
+
pub fn evaluate_zone(data: &ZoneAuditData) -> ZoneAuditReport {
|
|
9
|
+
let mut findings: Vec<AuditFinding> = Vec::new();
|
|
10
|
+
let mut passed_checks = 0usize;
|
|
11
|
+
let mut total_checks = 0usize;
|
|
12
|
+
|
|
13
|
+
// Helper macro to record finding
|
|
14
|
+
macro_rules! check {
|
|
15
|
+
($rule_id:expr, $passed:expr, $actual:expr, $expected:expr, $severity_override:expr, $penalty:expr) => {{
|
|
16
|
+
total_checks += 1;
|
|
17
|
+
if $passed {
|
|
18
|
+
passed_checks += 1;
|
|
19
|
+
} else {
|
|
20
|
+
if let Some(def) = get_rule_by_id($rule_id) {
|
|
21
|
+
let severity: RiskLevel = $severity_override.unwrap_or(def.default_severity);
|
|
22
|
+
findings.push(AuditFinding {
|
|
23
|
+
rule_id: def.id.to_string(),
|
|
24
|
+
rule_name: def.name.to_string(),
|
|
25
|
+
category: def.category,
|
|
26
|
+
risk_level: severity,
|
|
27
|
+
title: def.title.to_string(),
|
|
28
|
+
description: def.description.to_string(),
|
|
29
|
+
actual_value: $actual.to_string(),
|
|
30
|
+
expected_value: $expected.to_string(),
|
|
31
|
+
remediation: def.remediation.to_string(),
|
|
32
|
+
score_penalty: $penalty,
|
|
33
|
+
doc_url: Some(def.doc_url.to_string()),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 1. SSL/TLS Mode Check (CF-SSL-001, CF-SSL-002)
|
|
41
|
+
let ssl_mode = data
|
|
42
|
+
.settings
|
|
43
|
+
.ssl
|
|
44
|
+
.as_deref()
|
|
45
|
+
.unwrap_or("unknown")
|
|
46
|
+
.to_lowercase();
|
|
47
|
+
match ssl_mode.as_str() {
|
|
48
|
+
"off" => {
|
|
49
|
+
check!(
|
|
50
|
+
"CF-SSL-001",
|
|
51
|
+
false,
|
|
52
|
+
"off (SSL disabled)",
|
|
53
|
+
"strict (Full strict)",
|
|
54
|
+
Some(RiskLevel::Critical),
|
|
55
|
+
35
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
"flexible" => {
|
|
59
|
+
check!(
|
|
60
|
+
"CF-SSL-001",
|
|
61
|
+
false,
|
|
62
|
+
"flexible (Insecure edge-only encryption)",
|
|
63
|
+
"strict (Full strict)",
|
|
64
|
+
Some(RiskLevel::Critical),
|
|
65
|
+
30
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
"full" => {
|
|
69
|
+
check!(
|
|
70
|
+
"CF-SSL-002",
|
|
71
|
+
false,
|
|
72
|
+
"full (Non-strict origin certificate)",
|
|
73
|
+
"strict (Full strict)",
|
|
74
|
+
Some(RiskLevel::Medium),
|
|
75
|
+
10
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
"strict" => {
|
|
79
|
+
check!("CF-SSL-001", true, "strict", "strict", None, 0);
|
|
80
|
+
}
|
|
81
|
+
other => {
|
|
82
|
+
check!(
|
|
83
|
+
"CF-SSL-001",
|
|
84
|
+
false,
|
|
85
|
+
format!("unknown/unconfigured ({})", other),
|
|
86
|
+
"strict",
|
|
87
|
+
Some(RiskLevel::High),
|
|
88
|
+
25
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Minimum TLS Version (CF-TLS-001)
|
|
94
|
+
let min_tls = data
|
|
95
|
+
.settings
|
|
96
|
+
.min_tls_version
|
|
97
|
+
.as_deref()
|
|
98
|
+
.unwrap_or("1.0")
|
|
99
|
+
.to_string();
|
|
100
|
+
let min_tls_passed = min_tls == "1.2" || min_tls == "1.3";
|
|
101
|
+
check!(
|
|
102
|
+
"CF-TLS-001",
|
|
103
|
+
min_tls_passed,
|
|
104
|
+
format!("TLS {}", min_tls),
|
|
105
|
+
"TLS 1.2 or TLS 1.3",
|
|
106
|
+
if !min_tls_passed {
|
|
107
|
+
Some(RiskLevel::High)
|
|
108
|
+
} else {
|
|
109
|
+
None
|
|
110
|
+
},
|
|
111
|
+
if !min_tls_passed { 20 } else { 0 }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// 3. TLS 1.3 Protocol (CF-TLS-002)
|
|
115
|
+
let tls13_on = data
|
|
116
|
+
.settings
|
|
117
|
+
.tls_1_3
|
|
118
|
+
.as_deref()
|
|
119
|
+
.map(|v| v.eq_ignore_ascii_case("on") || v.eq_ignore_ascii_case("zrt"))
|
|
120
|
+
.unwrap_or(false);
|
|
121
|
+
check!(
|
|
122
|
+
"CF-TLS-002",
|
|
123
|
+
tls13_on,
|
|
124
|
+
if tls13_on { "on" } else { "off" },
|
|
125
|
+
"on",
|
|
126
|
+
Some(RiskLevel::Low),
|
|
127
|
+
if !tls13_on { 5 } else { 0 }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// 4. Always Use HTTPS (CF-HTTPS-001)
|
|
131
|
+
let always_https = data
|
|
132
|
+
.settings
|
|
133
|
+
.always_use_https
|
|
134
|
+
.as_deref()
|
|
135
|
+
.map(|v| v.eq_ignore_ascii_case("on"))
|
|
136
|
+
.unwrap_or(false);
|
|
137
|
+
check!(
|
|
138
|
+
"CF-HTTPS-001",
|
|
139
|
+
always_https,
|
|
140
|
+
if always_https { "on" } else { "off" },
|
|
141
|
+
"on",
|
|
142
|
+
Some(RiskLevel::High),
|
|
143
|
+
if !always_https { 20 } else { 0 }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// 5. Automatic HTTPS Rewrites (CF-HTTPS-002)
|
|
147
|
+
let auto_rewrites = data
|
|
148
|
+
.settings
|
|
149
|
+
.automatic_https_rewrites
|
|
150
|
+
.as_deref()
|
|
151
|
+
.map(|v| v.eq_ignore_ascii_case("on"))
|
|
152
|
+
.unwrap_or(false);
|
|
153
|
+
check!(
|
|
154
|
+
"CF-HTTPS-002",
|
|
155
|
+
auto_rewrites,
|
|
156
|
+
if auto_rewrites { "on" } else { "off" },
|
|
157
|
+
"on",
|
|
158
|
+
Some(RiskLevel::Medium),
|
|
159
|
+
if !auto_rewrites { 10 } else { 0 }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// 6. HSTS (CF-HSTS-001, CF-HSTS-002, CF-HSTS-003, CF-HSTS-004, CF-HSTS-005)
|
|
163
|
+
let hsts_opt = data
|
|
164
|
+
.settings
|
|
165
|
+
.security_header
|
|
166
|
+
.as_ref()
|
|
167
|
+
.and_then(|sh| sh.strict_transport_security.as_ref());
|
|
168
|
+
|
|
169
|
+
let hsts_enabled = hsts_opt.map(|h| h.enabled).unwrap_or(false);
|
|
170
|
+
check!(
|
|
171
|
+
"CF-HSTS-001",
|
|
172
|
+
hsts_enabled,
|
|
173
|
+
if hsts_enabled { "enabled" } else { "disabled" },
|
|
174
|
+
"enabled",
|
|
175
|
+
Some(RiskLevel::High),
|
|
176
|
+
if !hsts_enabled { 20 } else { 0 }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if hsts_enabled
|
|
180
|
+
&& let Some(hsts) = hsts_opt {
|
|
181
|
+
let max_age = hsts.max_age.unwrap_or(0);
|
|
182
|
+
let six_months = 15_552_000u64;
|
|
183
|
+
let one_year = 31_536_000u64;
|
|
184
|
+
|
|
185
|
+
if max_age < six_months {
|
|
186
|
+
check!(
|
|
187
|
+
"CF-HSTS-002",
|
|
188
|
+
false,
|
|
189
|
+
format!(
|
|
190
|
+
"{} seconds (~{:.1} months)",
|
|
191
|
+
max_age,
|
|
192
|
+
(max_age as f64) / 2_592_000.0
|
|
193
|
+
),
|
|
194
|
+
">= 15,552,000 seconds (6 months)",
|
|
195
|
+
Some(RiskLevel::Medium),
|
|
196
|
+
10
|
|
197
|
+
);
|
|
198
|
+
} else if max_age < one_year {
|
|
199
|
+
check!(
|
|
200
|
+
"CF-HSTS-002",
|
|
201
|
+
false,
|
|
202
|
+
format!(
|
|
203
|
+
"{} seconds (~{:.1} months)",
|
|
204
|
+
max_age,
|
|
205
|
+
(max_age as f64) / 2_592_000.0
|
|
206
|
+
),
|
|
207
|
+
">= 31,536,000 seconds (1 year / Preload ready)",
|
|
208
|
+
Some(RiskLevel::Low),
|
|
209
|
+
5
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
check!(
|
|
213
|
+
"CF-HSTS-002",
|
|
214
|
+
true,
|
|
215
|
+
format!("{}s", max_age),
|
|
216
|
+
">= 15,552,000s",
|
|
217
|
+
None,
|
|
218
|
+
0
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let subdomains = hsts.include_subdomains.unwrap_or(false);
|
|
223
|
+
check!(
|
|
224
|
+
"CF-HSTS-003",
|
|
225
|
+
subdomains,
|
|
226
|
+
if subdomains { "true" } else { "false" },
|
|
227
|
+
"true",
|
|
228
|
+
Some(RiskLevel::Medium),
|
|
229
|
+
if !subdomains { 10 } else { 0 }
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let preload = hsts.preload.unwrap_or(false);
|
|
233
|
+
check!(
|
|
234
|
+
"CF-HSTS-004",
|
|
235
|
+
preload,
|
|
236
|
+
if preload { "true" } else { "false" },
|
|
237
|
+
"true",
|
|
238
|
+
Some(RiskLevel::Low),
|
|
239
|
+
if !preload { 5 } else { 0 }
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
let nosniff = hsts.nosniff.unwrap_or(false);
|
|
243
|
+
check!(
|
|
244
|
+
"CF-HSTS-005",
|
|
245
|
+
nosniff,
|
|
246
|
+
if nosniff { "true" } else { "false" },
|
|
247
|
+
"true",
|
|
248
|
+
Some(RiskLevel::Low),
|
|
249
|
+
if !nosniff { 5 } else { 0 }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 7. WAF & Managed Rules (CF-WAF-001)
|
|
254
|
+
let waf_active = data.waf.waf_enabled
|
|
255
|
+
|| data.waf.managed_rules_active
|
|
256
|
+
|| !data.waf.packages.is_empty()
|
|
257
|
+
|| !data.waf.rulesets.is_empty();
|
|
258
|
+
check!(
|
|
259
|
+
"CF-WAF-001",
|
|
260
|
+
waf_active,
|
|
261
|
+
if waf_active {
|
|
262
|
+
"Active (Managed Rules/Rulesets Configured)"
|
|
263
|
+
} else {
|
|
264
|
+
"Disabled / Not Configured"
|
|
265
|
+
},
|
|
266
|
+
"Active (Cloudflare Managed Ruleset / OWASP enabled)",
|
|
267
|
+
Some(RiskLevel::High),
|
|
268
|
+
if !waf_active { 20 } else { 0 }
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// 8. Bot Fight Mode (CF-BOT-001)
|
|
272
|
+
let bot_active = data.bot_management.fight_mode
|
|
273
|
+
|| data.bot_management.sbfm_definitely_automated.is_some()
|
|
274
|
+
|| data.bot_management.using_latest_model.unwrap_or(false);
|
|
275
|
+
check!(
|
|
276
|
+
"CF-BOT-001",
|
|
277
|
+
bot_active,
|
|
278
|
+
if bot_active { "Active" } else { "Disabled" },
|
|
279
|
+
"Active (Bot Fight Mode / Super Bot Fight Mode)",
|
|
280
|
+
Some(RiskLevel::Medium),
|
|
281
|
+
if !bot_active { 10 } else { 0 }
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// 9. Rate Limiting (CF-RATE-001)
|
|
285
|
+
let rate_limits_active = !data.rate_limits.rules.is_empty()
|
|
286
|
+
&& data
|
|
287
|
+
.rate_limits
|
|
288
|
+
.rules
|
|
289
|
+
.iter()
|
|
290
|
+
.any(|r| !r.disabled.unwrap_or(false));
|
|
291
|
+
check!(
|
|
292
|
+
"CF-RATE-001",
|
|
293
|
+
rate_limits_active,
|
|
294
|
+
if rate_limits_active {
|
|
295
|
+
format!("{} active rules", data.rate_limits.rules.len())
|
|
296
|
+
} else {
|
|
297
|
+
"0 rules configured".to_string()
|
|
298
|
+
},
|
|
299
|
+
">= 1 Rate Limiting rule",
|
|
300
|
+
Some(RiskLevel::Low),
|
|
301
|
+
if !rate_limits_active { 5 } else { 0 }
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// 10. DNSSEC (CF-DNS-001)
|
|
305
|
+
let dnssec_active = data.dnssec.status.eq_ignore_ascii_case("active");
|
|
306
|
+
check!(
|
|
307
|
+
"CF-DNS-001",
|
|
308
|
+
dnssec_active,
|
|
309
|
+
&data.dnssec.status,
|
|
310
|
+
"active",
|
|
311
|
+
Some(RiskLevel::Medium),
|
|
312
|
+
if !dnssec_active { 10 } else { 0 }
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// 11. Overly Permissive IP Access Rules (CF-SEC-001)
|
|
316
|
+
let mut permissive_rule_found = false;
|
|
317
|
+
let mut permissive_reason = String::new();
|
|
318
|
+
let mut permissive_severity = RiskLevel::High;
|
|
319
|
+
let mut permissive_penalty = 20;
|
|
320
|
+
|
|
321
|
+
for rule in &data.ip_access_rules.rules {
|
|
322
|
+
if rule.paused.unwrap_or(false) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
let mode = rule.mode.to_lowercase();
|
|
326
|
+
if mode == "whitelist" || mode == "allow" || mode == "bypass" {
|
|
327
|
+
let target_val = rule.configuration.value.trim().to_lowercase();
|
|
328
|
+
if target_val == "0.0.0.0/0"
|
|
329
|
+
|| target_val == "::/0"
|
|
330
|
+
|| target_val == "0.0.0.0"
|
|
331
|
+
|| target_val == "any"
|
|
332
|
+
|| target_val == "*"
|
|
333
|
+
{
|
|
334
|
+
permissive_rule_found = true;
|
|
335
|
+
permissive_severity = RiskLevel::Critical;
|
|
336
|
+
permissive_penalty = 35;
|
|
337
|
+
permissive_reason = format!(
|
|
338
|
+
"Rule ID '{}' sets global '{}' for target '{}'",
|
|
339
|
+
rule.id, rule.mode, rule.configuration.value
|
|
340
|
+
);
|
|
341
|
+
break;
|
|
342
|
+
} else if target_val.ends_with("/0")
|
|
343
|
+
|| target_val.ends_with("/1")
|
|
344
|
+
|| target_val.ends_with("/2")
|
|
345
|
+
|| target_val.ends_with("/3")
|
|
346
|
+
|| target_val.ends_with("/4")
|
|
347
|
+
|| target_val.ends_with("/5")
|
|
348
|
+
|| target_val.ends_with("/6")
|
|
349
|
+
|| target_val.ends_with("/7")
|
|
350
|
+
|| target_val.ends_with("/8")
|
|
351
|
+
{
|
|
352
|
+
permissive_rule_found = true;
|
|
353
|
+
permissive_severity = RiskLevel::High;
|
|
354
|
+
permissive_penalty = 20;
|
|
355
|
+
permissive_reason = format!(
|
|
356
|
+
"Rule ID '{}' allows excessive subnet range '{}'",
|
|
357
|
+
rule.id, rule.configuration.value
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
check!(
|
|
364
|
+
"CF-SEC-001",
|
|
365
|
+
!permissive_rule_found,
|
|
366
|
+
if permissive_rule_found {
|
|
367
|
+
permissive_reason
|
|
368
|
+
} else {
|
|
369
|
+
"No overly broad IP access allow rules".to_string()
|
|
370
|
+
},
|
|
371
|
+
"Restricted specific IP addresses / narrow CIDRs",
|
|
372
|
+
Some(permissive_severity),
|
|
373
|
+
if permissive_rule_found {
|
|
374
|
+
permissive_penalty
|
|
375
|
+
} else {
|
|
376
|
+
0
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// 12. Zone Security Level (CF-SEC-002)
|
|
381
|
+
let sec_level = data
|
|
382
|
+
.settings
|
|
383
|
+
.security_level
|
|
384
|
+
.as_deref()
|
|
385
|
+
.unwrap_or("medium")
|
|
386
|
+
.to_lowercase();
|
|
387
|
+
match sec_level.as_str() {
|
|
388
|
+
"essentially_off" => {
|
|
389
|
+
check!(
|
|
390
|
+
"CF-SEC-002",
|
|
391
|
+
false,
|
|
392
|
+
"essentially_off",
|
|
393
|
+
"medium or high",
|
|
394
|
+
Some(RiskLevel::High),
|
|
395
|
+
15
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
"low" => {
|
|
399
|
+
check!(
|
|
400
|
+
"CF-SEC-002",
|
|
401
|
+
false,
|
|
402
|
+
"low",
|
|
403
|
+
"medium or high",
|
|
404
|
+
Some(RiskLevel::Medium),
|
|
405
|
+
5
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
"medium" | "high" | "under_attack" => {
|
|
409
|
+
check!("CF-SEC-002", true, &sec_level, "medium or high", None, 0);
|
|
410
|
+
}
|
|
411
|
+
_ => {
|
|
412
|
+
check!("CF-SEC-002", true, &sec_level, "medium or high", None, 0);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 13. Browser Integrity Check (CF-SEC-003)
|
|
417
|
+
let browser_check = data
|
|
418
|
+
.settings
|
|
419
|
+
.browser_check
|
|
420
|
+
.as_deref()
|
|
421
|
+
.map(|v| v.eq_ignore_ascii_case("on"))
|
|
422
|
+
.unwrap_or(true); // default in CF is usually on
|
|
423
|
+
check!(
|
|
424
|
+
"CF-SEC-003",
|
|
425
|
+
browser_check,
|
|
426
|
+
if browser_check { "on" } else { "off" },
|
|
427
|
+
"on",
|
|
428
|
+
Some(RiskLevel::Low),
|
|
429
|
+
if !browser_check { 5 } else { 0 }
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Calculate score & grade
|
|
433
|
+
let score = calculate_zone_score(&findings);
|
|
434
|
+
let grade = calculate_grade(score).to_string();
|
|
435
|
+
|
|
436
|
+
let settings_summary = ZoneSettingsSummary {
|
|
437
|
+
ssl_mode: data
|
|
438
|
+
.settings
|
|
439
|
+
.ssl
|
|
440
|
+
.clone()
|
|
441
|
+
.unwrap_or_else(|| "none".to_string()),
|
|
442
|
+
min_tls: format!("TLS {}", min_tls),
|
|
443
|
+
always_https: if always_https {
|
|
444
|
+
"Enabled".to_string()
|
|
445
|
+
} else {
|
|
446
|
+
"Disabled".to_string()
|
|
447
|
+
},
|
|
448
|
+
hsts_status: if hsts_enabled {
|
|
449
|
+
"Enabled".to_string()
|
|
450
|
+
} else {
|
|
451
|
+
"Disabled".to_string()
|
|
452
|
+
},
|
|
453
|
+
dnssec_status: data.dnssec.status.clone(),
|
|
454
|
+
waf_status: if waf_active {
|
|
455
|
+
"Active".to_string()
|
|
456
|
+
} else {
|
|
457
|
+
"Inactive".to_string()
|
|
458
|
+
},
|
|
459
|
+
bot_fight_mode: if bot_active {
|
|
460
|
+
"Active".to_string()
|
|
461
|
+
} else {
|
|
462
|
+
"Inactive".to_string()
|
|
463
|
+
},
|
|
464
|
+
security_level: sec_level,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
ZoneAuditReport {
|
|
468
|
+
zone_id: data.zone.id.clone(),
|
|
469
|
+
zone_name: data.zone.name.clone(),
|
|
470
|
+
plan_name: data
|
|
471
|
+
.zone
|
|
472
|
+
.plan
|
|
473
|
+
.as_ref()
|
|
474
|
+
.and_then(|p| p.name.clone())
|
|
475
|
+
.unwrap_or_else(|| "Free Plan".to_string()),
|
|
476
|
+
status: data.zone.status.clone(),
|
|
477
|
+
score,
|
|
478
|
+
grade,
|
|
479
|
+
passed_checks_count: passed_checks,
|
|
480
|
+
total_checks_count: total_checks,
|
|
481
|
+
settings_summary,
|
|
482
|
+
findings,
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
use crate::zone::models::{AuditFinding, RiskLevel};
|
|
2
|
+
|
|
3
|
+
/// Baseline maximum score for a zone with 0 security findings
|
|
4
|
+
pub const BASELINE_SCORE: u32 = 100;
|
|
5
|
+
|
|
6
|
+
/// Calculates health score (0-100) for a zone given its security findings
|
|
7
|
+
pub fn calculate_zone_score(findings: &[AuditFinding]) -> u32 {
|
|
8
|
+
let mut total_penalty: u32 = 0;
|
|
9
|
+
let mut has_critical = false;
|
|
10
|
+
|
|
11
|
+
for finding in findings {
|
|
12
|
+
total_penalty = total_penalty.saturating_add(finding.score_penalty);
|
|
13
|
+
if finding.risk_level == RiskLevel::Critical {
|
|
14
|
+
has_critical = true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let mut score = BASELINE_SCORE.saturating_sub(total_penalty);
|
|
19
|
+
|
|
20
|
+
// If any Critical risk exists (e.g. flexible SSL or 0.0.0.0/0 bypass),
|
|
21
|
+
// the zone cannot score higher than 49 (Failing grade)
|
|
22
|
+
if has_critical && score > 49 {
|
|
23
|
+
score = 49;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
score.min(100)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Converts a numerical health score (0-100) to a letter grade
|
|
30
|
+
pub fn calculate_grade(score: u32) -> &'static str {
|
|
31
|
+
match score {
|
|
32
|
+
95..=100 => "A+",
|
|
33
|
+
90..=94 => "A",
|
|
34
|
+
80..=89 => "B",
|
|
35
|
+
70..=79 => "C",
|
|
36
|
+
60..=69 => "D",
|
|
37
|
+
_ => "F",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Returns a human-friendly description of the security grade
|
|
42
|
+
pub fn grade_description(grade: &str) -> &'static str {
|
|
43
|
+
match grade {
|
|
44
|
+
"A+" => "Excellent Posture (Best-practice hardened)",
|
|
45
|
+
"A" => "Strong Posture (Compliant)",
|
|
46
|
+
"B" => "Good Posture (Minor enhancements recommended)",
|
|
47
|
+
"C" => "Moderate Risk (Several security gaps)",
|
|
48
|
+
"D" => "High Risk (Substantial vulnerabilities)",
|
|
49
|
+
"F" => "Critical Risk (Urgent remediation required)",
|
|
50
|
+
_ => "Unknown Posture",
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[cfg(test)]
|
|
55
|
+
mod tests {
|
|
56
|
+
use super::*;
|
|
57
|
+
use crate::zone::models::RuleCategory;
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn test_perfect_score() {
|
|
61
|
+
let findings = vec![];
|
|
62
|
+
assert_eq!(calculate_zone_score(&findings), 100);
|
|
63
|
+
assert_eq!(calculate_grade(100), "A+");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[test]
|
|
67
|
+
fn test_critical_finding_caps_score() {
|
|
68
|
+
let findings = vec![AuditFinding {
|
|
69
|
+
rule_id: "CF-SSL-001".to_string(),
|
|
70
|
+
rule_name: "SSLModeCheck".to_string(),
|
|
71
|
+
category: RuleCategory::SslTls,
|
|
72
|
+
risk_level: RiskLevel::Critical,
|
|
73
|
+
title: "Insecure SSL".to_string(),
|
|
74
|
+
description: "Flexible mode".to_string(),
|
|
75
|
+
actual_value: "flexible".to_string(),
|
|
76
|
+
expected_value: "strict".to_string(),
|
|
77
|
+
remediation: "Fix it".to_string(),
|
|
78
|
+
score_penalty: 30,
|
|
79
|
+
doc_url: None,
|
|
80
|
+
}];
|
|
81
|
+
let score = calculate_zone_score(&findings);
|
|
82
|
+
assert!(score <= 49);
|
|
83
|
+
assert_eq!(calculate_grade(score), "F");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_minor_deductions() {
|
|
88
|
+
let findings = vec![AuditFinding {
|
|
89
|
+
rule_id: "CF-TLS-002".to_string(),
|
|
90
|
+
rule_name: "Tls13Disabled".to_string(),
|
|
91
|
+
category: RuleCategory::SslTls,
|
|
92
|
+
risk_level: RiskLevel::Low,
|
|
93
|
+
title: "TLS 1.3 Disabled".to_string(),
|
|
94
|
+
description: "TLS 1.3 off".to_string(),
|
|
95
|
+
actual_value: "off".to_string(),
|
|
96
|
+
expected_value: "on".to_string(),
|
|
97
|
+
remediation: "Enable TLS 1.3".to_string(),
|
|
98
|
+
score_penalty: 5,
|
|
99
|
+
doc_url: None,
|
|
100
|
+
}];
|
|
101
|
+
assert_eq!(calculate_zone_score(&findings), 95);
|
|
102
|
+
assert_eq!(calculate_grade(95), "A+");
|
|
103
|
+
}
|
|
104
|
+
}
|