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,481 @@
|
|
|
1
|
+
use crate::zone::client::provider::ZoneDataProvider;
|
|
2
|
+
use crate::zone::models::{
|
|
3
|
+
AccountInfo, BotManagementSetting, DnssecSetting, HstsSetting, IpAccessConfig, IpAccessRule,
|
|
4
|
+
IpAccessRulesSetting, PlanInfo, RateLimitRule, RateLimitSetting, RulesetInfo,
|
|
5
|
+
SecurityHeaderSetting, WafPackage, WafSetting, Zone, ZoneAuditData, ZoneLockdownRule,
|
|
6
|
+
ZoneLockdownSetting, ZoneSettings,
|
|
7
|
+
};
|
|
8
|
+
use anyhow::{Context, Result};
|
|
9
|
+
use std::future::Future;
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
use std::pin::Pin;
|
|
12
|
+
|
|
13
|
+
/// Generates built-in synthetic mock zones covering different security configurations
|
|
14
|
+
pub fn get_mock_zones() -> Vec<ZoneAuditData> {
|
|
15
|
+
vec![
|
|
16
|
+
// Zone 1: Hardened Enterprise Grade Zone (Score: 100, Grade: A+)
|
|
17
|
+
ZoneAuditData {
|
|
18
|
+
zone: Zone {
|
|
19
|
+
id: "11111111111111111111111111111111".to_string(),
|
|
20
|
+
name: "prod-banking.example.com".to_string(),
|
|
21
|
+
status: "active".to_string(),
|
|
22
|
+
paused: false,
|
|
23
|
+
zone_type: Some("full".to_string()),
|
|
24
|
+
development_mode: Some(0),
|
|
25
|
+
name_servers: Some(vec![
|
|
26
|
+
"ns1.cloudflare.com".to_string(),
|
|
27
|
+
"ns2.cloudflare.com".to_string(),
|
|
28
|
+
]),
|
|
29
|
+
account: Some(AccountInfo {
|
|
30
|
+
id: "acc_enterprise_999".to_string(),
|
|
31
|
+
name: "Apex Financial Group".to_string(),
|
|
32
|
+
}),
|
|
33
|
+
plan: Some(PlanInfo {
|
|
34
|
+
id: Some("enterprise".to_string()),
|
|
35
|
+
name: Some("Enterprise Plan".to_string()),
|
|
36
|
+
price: Some(5000),
|
|
37
|
+
currency: Some("USD".to_string()),
|
|
38
|
+
is_subscribed: Some(true),
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
settings: ZoneSettings {
|
|
42
|
+
ssl: Some("strict".to_string()),
|
|
43
|
+
min_tls_version: Some("1.3".to_string()),
|
|
44
|
+
tls_1_3: Some("on".to_string()),
|
|
45
|
+
always_use_https: Some("on".to_string()),
|
|
46
|
+
automatic_https_rewrites: Some("on".to_string()),
|
|
47
|
+
opportunistic_encryption: Some("on".to_string()),
|
|
48
|
+
security_header: Some(SecurityHeaderSetting {
|
|
49
|
+
strict_transport_security: Some(HstsSetting {
|
|
50
|
+
enabled: true,
|
|
51
|
+
max_age: Some(31536000), // 1 year
|
|
52
|
+
include_subdomains: Some(true),
|
|
53
|
+
preload: Some(true),
|
|
54
|
+
nosniff: Some(true),
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
security_level: Some("high".to_string()),
|
|
58
|
+
browser_check: Some("on".to_string()),
|
|
59
|
+
challenge_ttl: Some(1800),
|
|
60
|
+
brotli: Some("on".to_string()),
|
|
61
|
+
early_hints: Some("on".to_string()),
|
|
62
|
+
},
|
|
63
|
+
dnssec: DnssecSetting {
|
|
64
|
+
status: "active".to_string(),
|
|
65
|
+
flags: Some(257),
|
|
66
|
+
algorithm: Some("13".to_string()),
|
|
67
|
+
key_type: Some("KSK".to_string()),
|
|
68
|
+
digest_type: Some("2".to_string()),
|
|
69
|
+
digest_algorithm: Some("SHA-256".to_string()),
|
|
70
|
+
digest: Some("E2D3C4B5A6...".to_string()),
|
|
71
|
+
ds: Some("prod-banking.example.com. IN DS 2371 13 2 E2D3C...".to_string()),
|
|
72
|
+
key_tag: Some(2371),
|
|
73
|
+
public_key: Some("mdssw58R58...".to_string()),
|
|
74
|
+
},
|
|
75
|
+
waf: WafSetting {
|
|
76
|
+
waf_enabled: true,
|
|
77
|
+
managed_rules_active: true,
|
|
78
|
+
packages: vec![WafPackage {
|
|
79
|
+
id: "pkg_cf_core".to_string(),
|
|
80
|
+
name: "Cloudflare Managed Ruleset".to_string(),
|
|
81
|
+
description: Some("Core managed rules".to_string()),
|
|
82
|
+
detection_mode: Some("anomaly".to_string()),
|
|
83
|
+
zone_id: Some("11111111111111111111111111111111".to_string()),
|
|
84
|
+
status: Some("active".to_string()),
|
|
85
|
+
}],
|
|
86
|
+
rulesets: vec![RulesetInfo {
|
|
87
|
+
id: "rs_owasp".to_string(),
|
|
88
|
+
name: "Cloudflare OWASP Core Ruleset".to_string(),
|
|
89
|
+
phase: Some("http_request_firewall_managed".to_string()),
|
|
90
|
+
kind: Some("managed".to_string()),
|
|
91
|
+
description: Some("OWASP Top 10 protection".to_string()),
|
|
92
|
+
rules_count: Some(48),
|
|
93
|
+
}],
|
|
94
|
+
},
|
|
95
|
+
bot_management: BotManagementSetting {
|
|
96
|
+
fight_mode: true,
|
|
97
|
+
using_latest_model: Some(true),
|
|
98
|
+
optimize_wordpress: Some(false),
|
|
99
|
+
sbfm_definitely_automated: Some("block".to_string()),
|
|
100
|
+
sbfm_likely_automated: Some("managed_challenge".to_string()),
|
|
101
|
+
sbfm_verified_bots: Some("allow".to_string()),
|
|
102
|
+
sbfm_static_resource_protection: Some(true),
|
|
103
|
+
},
|
|
104
|
+
rate_limits: RateLimitSetting {
|
|
105
|
+
rules: vec![
|
|
106
|
+
RateLimitRule {
|
|
107
|
+
id: "rl_login_bruteforce".to_string(),
|
|
108
|
+
disabled: Some(false),
|
|
109
|
+
description: Some(
|
|
110
|
+
"Protect /api/v1/auth/login from brute force".to_string(),
|
|
111
|
+
),
|
|
112
|
+
threshold: Some(5),
|
|
113
|
+
period: Some(60),
|
|
114
|
+
action: Some("challenge".to_string()),
|
|
115
|
+
},
|
|
116
|
+
RateLimitRule {
|
|
117
|
+
id: "rl_api_rate_limit".to_string(),
|
|
118
|
+
disabled: Some(false),
|
|
119
|
+
description: Some("Global API rate limiting".to_string()),
|
|
120
|
+
threshold: Some(100),
|
|
121
|
+
period: Some(60),
|
|
122
|
+
action: Some("block".to_string()),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
lockdowns: ZoneLockdownSetting {
|
|
127
|
+
rules: vec![ZoneLockdownRule {
|
|
128
|
+
id: "lock_admin_portal".to_string(),
|
|
129
|
+
paused: Some(false),
|
|
130
|
+
description: Some("Restrict /admin/* to VPN gateway".to_string()),
|
|
131
|
+
urls: vec!["prod-banking.example.com/admin/*".to_string()],
|
|
132
|
+
configurations: vec![crate::zone::models::LockdownConfig {
|
|
133
|
+
target: "ip".to_string(),
|
|
134
|
+
value: "198.51.100.50".to_string(),
|
|
135
|
+
}],
|
|
136
|
+
}],
|
|
137
|
+
},
|
|
138
|
+
ip_access_rules: IpAccessRulesSetting {
|
|
139
|
+
rules: vec![IpAccessRule {
|
|
140
|
+
id: "ip_rule_corp_hq".to_string(),
|
|
141
|
+
mode: "whitelist".to_string(),
|
|
142
|
+
configuration: IpAccessConfig {
|
|
143
|
+
target: "ip".to_string(),
|
|
144
|
+
value: "198.51.100.10".to_string(),
|
|
145
|
+
},
|
|
146
|
+
notes: Some("Corporate Headquarters Egress IP".to_string()),
|
|
147
|
+
paused: Some(false),
|
|
148
|
+
}],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
// Zone 2: Moderate / Good E-Commerce Shop (Score: ~85, Grade: B)
|
|
152
|
+
ZoneAuditData {
|
|
153
|
+
zone: Zone {
|
|
154
|
+
id: "22222222222222222222222222222222".to_string(),
|
|
155
|
+
name: "ecommerce-store.io".to_string(),
|
|
156
|
+
status: "active".to_string(),
|
|
157
|
+
paused: false,
|
|
158
|
+
zone_type: Some("full".to_string()),
|
|
159
|
+
development_mode: Some(0),
|
|
160
|
+
name_servers: Some(vec![
|
|
161
|
+
"ns1.cloudflare.com".to_string(),
|
|
162
|
+
"ns2.cloudflare.com".to_string(),
|
|
163
|
+
]),
|
|
164
|
+
account: Some(AccountInfo {
|
|
165
|
+
id: "acc_retail_456".to_string(),
|
|
166
|
+
name: "Direct Retailers LLC".to_string(),
|
|
167
|
+
}),
|
|
168
|
+
plan: Some(PlanInfo {
|
|
169
|
+
id: Some("pro".to_string()),
|
|
170
|
+
name: Some("Pro Plan".to_string()),
|
|
171
|
+
price: Some(25),
|
|
172
|
+
currency: Some("USD".to_string()),
|
|
173
|
+
is_subscribed: Some(true),
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
settings: ZoneSettings {
|
|
177
|
+
ssl: Some("full".to_string()), // Triggers CF-SSL-002 (-10 pts)
|
|
178
|
+
min_tls_version: Some("1.2".to_string()),
|
|
179
|
+
tls_1_3: Some("on".to_string()),
|
|
180
|
+
always_use_https: Some("on".to_string()),
|
|
181
|
+
automatic_https_rewrites: Some("on".to_string()),
|
|
182
|
+
opportunistic_encryption: Some("on".to_string()),
|
|
183
|
+
security_header: Some(SecurityHeaderSetting {
|
|
184
|
+
strict_transport_security: Some(HstsSetting {
|
|
185
|
+
enabled: true,
|
|
186
|
+
max_age: Some(15552000), // 6 months -> triggers CF-HSTS-002 Low (-5 pts)
|
|
187
|
+
include_subdomains: Some(true),
|
|
188
|
+
preload: Some(false), // triggers CF-HSTS-004 (-5 pts)
|
|
189
|
+
nosniff: Some(true),
|
|
190
|
+
}),
|
|
191
|
+
}),
|
|
192
|
+
security_level: Some("medium".to_string()),
|
|
193
|
+
browser_check: Some("on".to_string()),
|
|
194
|
+
challenge_ttl: Some(3600),
|
|
195
|
+
brotli: Some("on".to_string()),
|
|
196
|
+
early_hints: Some("off".to_string()),
|
|
197
|
+
},
|
|
198
|
+
dnssec: DnssecSetting {
|
|
199
|
+
status: "active".to_string(),
|
|
200
|
+
flags: Some(257),
|
|
201
|
+
algorithm: Some("13".to_string()),
|
|
202
|
+
key_type: Some("KSK".to_string()),
|
|
203
|
+
digest_type: Some("2".to_string()),
|
|
204
|
+
digest_algorithm: Some("SHA-256".to_string()),
|
|
205
|
+
digest: Some("A1B2C3...".to_string()),
|
|
206
|
+
ds: Some("ecommerce-store.io. IN DS ...".to_string()),
|
|
207
|
+
key_tag: Some(1234),
|
|
208
|
+
public_key: Some("pubkey...".to_string()),
|
|
209
|
+
},
|
|
210
|
+
waf: WafSetting {
|
|
211
|
+
waf_enabled: true,
|
|
212
|
+
managed_rules_active: true,
|
|
213
|
+
packages: vec![WafPackage {
|
|
214
|
+
id: "pkg_cf_core".to_string(),
|
|
215
|
+
name: "Cloudflare Managed Ruleset".to_string(),
|
|
216
|
+
description: Some("Core managed rules".to_string()),
|
|
217
|
+
detection_mode: Some("anomaly".to_string()),
|
|
218
|
+
zone_id: Some("22222222222222222222222222222222".to_string()),
|
|
219
|
+
status: Some("active".to_string()),
|
|
220
|
+
}],
|
|
221
|
+
rulesets: vec![],
|
|
222
|
+
},
|
|
223
|
+
bot_management: BotManagementSetting {
|
|
224
|
+
fight_mode: true,
|
|
225
|
+
using_latest_model: Some(false),
|
|
226
|
+
optimize_wordpress: Some(false),
|
|
227
|
+
sbfm_definitely_automated: None,
|
|
228
|
+
sbfm_likely_automated: None,
|
|
229
|
+
sbfm_verified_bots: None,
|
|
230
|
+
sbfm_static_resource_protection: None,
|
|
231
|
+
},
|
|
232
|
+
rate_limits: RateLimitSetting {
|
|
233
|
+
rules: vec![RateLimitRule {
|
|
234
|
+
id: "rl_checkout".to_string(),
|
|
235
|
+
disabled: Some(false),
|
|
236
|
+
description: Some("Rate limit checkout submissions".to_string()),
|
|
237
|
+
threshold: Some(10),
|
|
238
|
+
period: Some(60),
|
|
239
|
+
action: Some("challenge".to_string()),
|
|
240
|
+
}],
|
|
241
|
+
},
|
|
242
|
+
lockdowns: ZoneLockdownSetting::default(),
|
|
243
|
+
ip_access_rules: IpAccessRulesSetting::default(),
|
|
244
|
+
},
|
|
245
|
+
// Zone 3: Severely Insecure Legacy Portal (Score: < 20, Grade: F, Multiple Critical & High)
|
|
246
|
+
ZoneAuditData {
|
|
247
|
+
zone: Zone {
|
|
248
|
+
id: "33333333333333333333333333333333".to_string(),
|
|
249
|
+
name: "legacy-portal.example.org".to_string(),
|
|
250
|
+
status: "active".to_string(),
|
|
251
|
+
paused: false,
|
|
252
|
+
zone_type: Some("full".to_string()),
|
|
253
|
+
development_mode: Some(0),
|
|
254
|
+
name_servers: Some(vec![
|
|
255
|
+
"ns1.cloudflare.com".to_string(),
|
|
256
|
+
"ns2.cloudflare.com".to_string(),
|
|
257
|
+
]),
|
|
258
|
+
account: Some(AccountInfo {
|
|
259
|
+
id: "acc_legacy_111".to_string(),
|
|
260
|
+
name: "Legacy Operations".to_string(),
|
|
261
|
+
}),
|
|
262
|
+
plan: Some(PlanInfo {
|
|
263
|
+
id: Some("free".to_string()),
|
|
264
|
+
name: Some("Free Plan".to_string()),
|
|
265
|
+
price: Some(0),
|
|
266
|
+
currency: Some("USD".to_string()),
|
|
267
|
+
is_subscribed: Some(false),
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
settings: ZoneSettings {
|
|
271
|
+
ssl: Some("flexible".to_string()), // CRITICAL: CF-SSL-001 (-30 pts, cap at 49)
|
|
272
|
+
min_tls_version: Some("1.0".to_string()), // HIGH: CF-TLS-001 (-20 pts)
|
|
273
|
+
tls_1_3: Some("off".to_string()), // LOW: CF-TLS-002 (-5 pts)
|
|
274
|
+
always_use_https: Some("off".to_string()), // HIGH: CF-HTTPS-001 (-20 pts)
|
|
275
|
+
automatic_https_rewrites: Some("off".to_string()), // MEDIUM: CF-HTTPS-002 (-10 pts)
|
|
276
|
+
opportunistic_encryption: Some("off".to_string()),
|
|
277
|
+
security_header: Some(SecurityHeaderSetting {
|
|
278
|
+
strict_transport_security: Some(HstsSetting {
|
|
279
|
+
enabled: false, // HIGH: CF-HSTS-001 (-20 pts)
|
|
280
|
+
max_age: Some(0),
|
|
281
|
+
include_subdomains: Some(false),
|
|
282
|
+
preload: Some(false),
|
|
283
|
+
nosniff: Some(false),
|
|
284
|
+
}),
|
|
285
|
+
}),
|
|
286
|
+
security_level: Some("essentially_off".to_string()), // HIGH: CF-SEC-002 (-15 pts)
|
|
287
|
+
browser_check: Some("off".to_string()), // LOW: CF-SEC-003 (-5 pts)
|
|
288
|
+
challenge_ttl: Some(86400),
|
|
289
|
+
brotli: Some("off".to_string()),
|
|
290
|
+
early_hints: Some("off".to_string()),
|
|
291
|
+
},
|
|
292
|
+
dnssec: DnssecSetting {
|
|
293
|
+
status: "disabled".to_string(), // MEDIUM: CF-DNS-001 (-10 pts)
|
|
294
|
+
..Default::default()
|
|
295
|
+
},
|
|
296
|
+
waf: WafSetting {
|
|
297
|
+
waf_enabled: false, // HIGH: CF-WAF-001 (-20 pts)
|
|
298
|
+
managed_rules_active: false,
|
|
299
|
+
packages: vec![],
|
|
300
|
+
rulesets: vec![],
|
|
301
|
+
},
|
|
302
|
+
bot_management: BotManagementSetting {
|
|
303
|
+
fight_mode: false, // MEDIUM: CF-BOT-001 (-10 pts)
|
|
304
|
+
..Default::default()
|
|
305
|
+
},
|
|
306
|
+
rate_limits: RateLimitSetting {
|
|
307
|
+
rules: vec![], // LOW: CF-RATE-001 (-5 pts)
|
|
308
|
+
},
|
|
309
|
+
lockdowns: ZoneLockdownSetting::default(),
|
|
310
|
+
ip_access_rules: IpAccessRulesSetting {
|
|
311
|
+
rules: vec![IpAccessRule {
|
|
312
|
+
id: "ip_rule_wildcard_bypass".to_string(),
|
|
313
|
+
mode: "whitelist".to_string(),
|
|
314
|
+
configuration: IpAccessConfig {
|
|
315
|
+
target: "ip_range".to_string(),
|
|
316
|
+
value: "0.0.0.0/0".to_string(), // CRITICAL: CF-SEC-001 (-35 pts)
|
|
317
|
+
},
|
|
318
|
+
notes: Some("Temporary blanket bypass - forgotten in prod".to_string()),
|
|
319
|
+
paused: Some(false),
|
|
320
|
+
}],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
// Zone 4: Development / Staging Gateway (Score: ~55-65, Grade: D)
|
|
324
|
+
ZoneAuditData {
|
|
325
|
+
zone: Zone {
|
|
326
|
+
id: "44444444444444444444444444444444".to_string(),
|
|
327
|
+
name: "dev-api-gateway.net".to_string(),
|
|
328
|
+
status: "active".to_string(),
|
|
329
|
+
paused: false,
|
|
330
|
+
zone_type: Some("full".to_string()),
|
|
331
|
+
development_mode: Some(1),
|
|
332
|
+
name_servers: Some(vec![
|
|
333
|
+
"ns1.cloudflare.com".to_string(),
|
|
334
|
+
"ns2.cloudflare.com".to_string(),
|
|
335
|
+
]),
|
|
336
|
+
account: Some(AccountInfo {
|
|
337
|
+
id: "acc_dev_222".to_string(),
|
|
338
|
+
name: "Internal Engineering".to_string(),
|
|
339
|
+
}),
|
|
340
|
+
plan: Some(PlanInfo {
|
|
341
|
+
id: Some("business".to_string()),
|
|
342
|
+
name: Some("Business Plan".to_string()),
|
|
343
|
+
price: Some(200),
|
|
344
|
+
currency: Some("USD".to_string()),
|
|
345
|
+
is_subscribed: Some(true),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
settings: ZoneSettings {
|
|
349
|
+
ssl: Some("strict".to_string()),
|
|
350
|
+
min_tls_version: Some("1.2".to_string()),
|
|
351
|
+
tls_1_3: Some("on".to_string()),
|
|
352
|
+
always_use_https: Some("on".to_string()),
|
|
353
|
+
automatic_https_rewrites: Some("off".to_string()), // MEDIUM (-10 pts)
|
|
354
|
+
opportunistic_encryption: Some("on".to_string()),
|
|
355
|
+
security_header: Some(SecurityHeaderSetting {
|
|
356
|
+
strict_transport_security: Some(HstsSetting {
|
|
357
|
+
enabled: false, // HIGH (-20 pts)
|
|
358
|
+
max_age: None,
|
|
359
|
+
include_subdomains: Some(false),
|
|
360
|
+
preload: Some(false),
|
|
361
|
+
nosniff: Some(false),
|
|
362
|
+
}),
|
|
363
|
+
}),
|
|
364
|
+
security_level: Some("low".to_string()), // MEDIUM (-5 pts)
|
|
365
|
+
browser_check: Some("on".to_string()),
|
|
366
|
+
challenge_ttl: Some(3600),
|
|
367
|
+
brotli: Some("on".to_string()),
|
|
368
|
+
early_hints: Some("off".to_string()),
|
|
369
|
+
},
|
|
370
|
+
dnssec: DnssecSetting {
|
|
371
|
+
status: "pending".to_string(), // MEDIUM (-10 pts)
|
|
372
|
+
..Default::default()
|
|
373
|
+
},
|
|
374
|
+
waf: WafSetting {
|
|
375
|
+
waf_enabled: true,
|
|
376
|
+
managed_rules_active: true,
|
|
377
|
+
packages: vec![WafPackage {
|
|
378
|
+
id: "pkg_cf_core".to_string(),
|
|
379
|
+
name: "Cloudflare Managed Ruleset".to_string(),
|
|
380
|
+
description: Some("Core rules".to_string()),
|
|
381
|
+
detection_mode: Some("anomaly".to_string()),
|
|
382
|
+
zone_id: Some("44444444444444444444444444444444".to_string()),
|
|
383
|
+
status: Some("active".to_string()),
|
|
384
|
+
}],
|
|
385
|
+
rulesets: vec![],
|
|
386
|
+
},
|
|
387
|
+
bot_management: BotManagementSetting {
|
|
388
|
+
fight_mode: false, // MEDIUM (-10 pts)
|
|
389
|
+
..Default::default()
|
|
390
|
+
},
|
|
391
|
+
rate_limits: RateLimitSetting {
|
|
392
|
+
rules: vec![], // LOW (-5 pts)
|
|
393
|
+
},
|
|
394
|
+
lockdowns: ZoneLockdownSetting::default(),
|
|
395
|
+
ip_access_rules: IpAccessRulesSetting::default(),
|
|
396
|
+
},
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Reads mock zone configurations from a JSON file
|
|
401
|
+
pub fn load_mock_from_file(path: impl AsRef<Path>) -> Result<Vec<ZoneAuditData>> {
|
|
402
|
+
let p = path.as_ref();
|
|
403
|
+
let content = std::fs::read_to_string(p)
|
|
404
|
+
.with_context(|| format!("Failed to read mock file from '{}'", p.display()))?;
|
|
405
|
+
|
|
406
|
+
// Try parsing as array of ZoneAuditData
|
|
407
|
+
if let Ok(zones) = serde_json::from_str::<Vec<ZoneAuditData>>(&content) {
|
|
408
|
+
return Ok(zones);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Try parsing as single ZoneAuditData
|
|
412
|
+
if let Ok(single_zone) = serde_json::from_str::<ZoneAuditData>(&content) {
|
|
413
|
+
return Ok(vec![single_zone]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Try parsing wrapped object { "zones": [...] }
|
|
417
|
+
#[derive(serde::Deserialize)]
|
|
418
|
+
struct WrappedZones {
|
|
419
|
+
zones: Vec<ZoneAuditData>,
|
|
420
|
+
}
|
|
421
|
+
if let Ok(wrapped) = serde_json::from_str::<WrappedZones>(&content) {
|
|
422
|
+
return Ok(wrapped.zones);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
anyhow::bail!(
|
|
426
|
+
"Failed to deserialize mock zone data from '{}'. Expected JSON format containing array of ZoneAuditData.",
|
|
427
|
+
p.display()
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/// Serializes default mock dataset to pretty-printed JSON string
|
|
432
|
+
pub fn generate_sample_mock_json() -> String {
|
|
433
|
+
serde_json::to_string_pretty(&get_mock_zones()).unwrap_or_else(|_| "[]".to_string())
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// Mock implementation of ZoneDataProvider
|
|
437
|
+
pub struct MockZoneProvider {
|
|
438
|
+
zones: Vec<ZoneAuditData>,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
impl MockZoneProvider {
|
|
442
|
+
pub fn new_builtin() -> Self {
|
|
443
|
+
Self {
|
|
444
|
+
zones: get_mock_zones(),
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
|
449
|
+
let zones = load_mock_from_file(path)?;
|
|
450
|
+
Ok(Self { zones })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pub fn from_zones(zones: Vec<ZoneAuditData>) -> Self {
|
|
454
|
+
Self { zones }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
impl ZoneDataProvider for MockZoneProvider {
|
|
459
|
+
fn fetch_all_zones<'a>(
|
|
460
|
+
&'a self,
|
|
461
|
+
zone_filter: Option<&'a str>,
|
|
462
|
+
) -> Pin<Box<dyn Future<Output = Result<Vec<ZoneAuditData>>> + Send + 'a>> {
|
|
463
|
+
Box::pin(async move {
|
|
464
|
+
if let Some(filter) = zone_filter
|
|
465
|
+
&& !filter.is_empty() {
|
|
466
|
+
let filtered: Vec<ZoneAuditData> = self
|
|
467
|
+
.zones
|
|
468
|
+
.iter()
|
|
469
|
+
.filter(|z| {
|
|
470
|
+
z.zone.name.eq_ignore_ascii_case(filter)
|
|
471
|
+
|| z.zone.id.eq_ignore_ascii_case(filter)
|
|
472
|
+
|| z.zone.name.to_lowercase().contains(&filter.to_lowercase())
|
|
473
|
+
})
|
|
474
|
+
.cloned()
|
|
475
|
+
.collect();
|
|
476
|
+
return Ok(filtered);
|
|
477
|
+
}
|
|
478
|
+
Ok(self.zones.clone())
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
}
|
package/src/zone/mod.rs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
pub mod cli;
|
|
2
|
+
pub mod client;
|
|
3
|
+
pub mod mock_data;
|
|
4
|
+
pub mod models;
|
|
5
|
+
pub mod reporters;
|
|
6
|
+
pub mod rules;
|
|
7
|
+
pub mod scoring;
|
|
8
|
+
|
|
9
|
+
use anyhow::{Context, Result, bail};
|
|
10
|
+
use cli::{AuditArgs, OutputFormat};
|
|
11
|
+
use client::{CloudflareClient, ZoneDataProvider};
|
|
12
|
+
use mock_data::MockZoneProvider;
|
|
13
|
+
use models::AggregateAuditReport;
|
|
14
|
+
use rules::evaluate_zone;
|
|
15
|
+
use std::fs;
|
|
16
|
+
|
|
17
|
+
/// Core auditor execution engine
|
|
18
|
+
pub async fn run_audit(args: &AuditArgs) -> Result<AggregateAuditReport> {
|
|
19
|
+
let provider: Box<dyn ZoneDataProvider> = if let Some(ref input_path) = args.input {
|
|
20
|
+
Box::new(MockZoneProvider::from_file(input_path)?)
|
|
21
|
+
} else if args.mock {
|
|
22
|
+
Box::new(MockZoneProvider::new_builtin())
|
|
23
|
+
} else if let Some(ref token) = args.token {
|
|
24
|
+
Box::new(CloudflareClient::new(token, args.account_id.clone())?)
|
|
25
|
+
} else {
|
|
26
|
+
bail!(
|
|
27
|
+
"Missing authentication. Please provide a Cloudflare API token via '--token <TOKEN>' \
|
|
28
|
+
or 'CF_API_TOKEN' environment variable, or use '--mock' / '--input <FILE>' for offline auditing."
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let zone_filter = args.zone.as_deref();
|
|
33
|
+
let zone_data_list = provider
|
|
34
|
+
.fetch_all_zones(zone_filter)
|
|
35
|
+
.await
|
|
36
|
+
.context("Failed to fetch zone data")?;
|
|
37
|
+
|
|
38
|
+
if zone_data_list.is_empty() {
|
|
39
|
+
if let Some(filter) = zone_filter {
|
|
40
|
+
bail!("No zones found matching filter '{}'", filter);
|
|
41
|
+
} else {
|
|
42
|
+
bail!("No zones found in the Cloudflare account");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let mut zone_reports = Vec::with_capacity(zone_data_list.len());
|
|
47
|
+
for data in &zone_data_list {
|
|
48
|
+
zone_reports.push(evaluate_zone(data));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let aggregate = AggregateAuditReport::new(zone_reports, args.account_id.clone());
|
|
52
|
+
Ok(aggregate)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Formats and outputs the aggregate audit report according to CLI arguments
|
|
56
|
+
pub fn output_report(report: &AggregateAuditReport, args: &AuditArgs) -> Result<String> {
|
|
57
|
+
let output_str = match args.format {
|
|
58
|
+
OutputFormat::Table => reporters::render_terminal(report, args.verbose),
|
|
59
|
+
OutputFormat::Json => reporters::render_json(report)?,
|
|
60
|
+
OutputFormat::Html => reporters::render_html(report),
|
|
61
|
+
OutputFormat::Sarif => reporters::render_sarif(report)?,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if let Some(ref out_path) = args.output {
|
|
65
|
+
fs::write(out_path, &output_str)
|
|
66
|
+
.with_context(|| format!("Failed to write report to '{}'", out_path.display()))?;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Ok(output_str)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Evaluates CI compliance gates and returns true if passing, false if failed
|
|
73
|
+
pub fn evaluate_compliance_gates(
|
|
74
|
+
report: &AggregateAuditReport,
|
|
75
|
+
args: &AuditArgs,
|
|
76
|
+
) -> (bool, Vec<String>) {
|
|
77
|
+
let mut passed = true;
|
|
78
|
+
let mut failure_reasons = Vec::new();
|
|
79
|
+
|
|
80
|
+
let fail_on_critical = args.fail_on_critical || args.check;
|
|
81
|
+
let fail_on_high = args.fail_on_high || args.check;
|
|
82
|
+
let min_score = if args.check && args.min_score.is_none() {
|
|
83
|
+
Some(80)
|
|
84
|
+
} else {
|
|
85
|
+
args.min_score
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if fail_on_critical && report.total_findings.critical > 0 {
|
|
89
|
+
passed = false;
|
|
90
|
+
failure_reasons.push(format!(
|
|
91
|
+
"Failed CI Gate: Found {} CRITICAL severity security finding(s)",
|
|
92
|
+
report.total_findings.critical
|
|
93
|
+
));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if fail_on_high && report.total_findings.high > 0 {
|
|
97
|
+
passed = false;
|
|
98
|
+
failure_reasons.push(format!(
|
|
99
|
+
"Failed CI Gate: Found {} HIGH severity security finding(s)",
|
|
100
|
+
report.total_findings.high
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if let Some(min) = min_score {
|
|
105
|
+
if report.average_score < (min as f64) {
|
|
106
|
+
passed = false;
|
|
107
|
+
failure_reasons.push(format!(
|
|
108
|
+
"Failed CI Gate: Account security score {:.1} is below minimum threshold of {}",
|
|
109
|
+
report.average_score, min
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for z in &report.zone_reports {
|
|
114
|
+
if z.score < min {
|
|
115
|
+
passed = false;
|
|
116
|
+
failure_reasons.push(format!(
|
|
117
|
+
"Failed CI Gate: Zone '{}' security score {} is below minimum threshold of {}",
|
|
118
|
+
z.zone_name, z.score, min
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
(passed, failure_reasons)
|
|
125
|
+
}
|