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,111 @@
1
+ use clap::{Args, Parser, Subcommand, ValueEnum};
2
+ use std::path::PathBuf;
3
+
4
+ #[derive(Debug, Parser)]
5
+ #[command(
6
+ name = "cf-zone-auditor",
7
+ author = "Antigravity Systems",
8
+ version = env!("CARGO_PKG_VERSION"),
9
+ about = "Cloudflare Zone Security Posture Auditor & Compliance Gate",
10
+ long_about = "Fast, comprehensive security posture auditor for Cloudflare zones.\nScans SSL/TLS modes, TLS versions, HSTS headers, HTTPS enforcement, WAF managed rules,\nBot Fight Mode, rate limiting, DNSSEC, and IP access rules."
11
+ )]
12
+ pub struct Cli {
13
+ #[command(subcommand)]
14
+ pub command: Option<Commands>,
15
+
16
+ #[command(flatten)]
17
+ pub audit_args: AuditArgs,
18
+ }
19
+
20
+ #[derive(Debug, Subcommand)]
21
+ pub enum Commands {
22
+ /// Run security posture audit on Cloudflare zones (default command)
23
+ Audit(AuditArgs),
24
+
25
+ /// Generate a sample mock JSON file with multiple realistic zone configurations
26
+ GenerateMock {
27
+ /// File path to write the sample JSON to (prints to stdout if omitted)
28
+ #[arg(short, long)]
29
+ output: Option<PathBuf>,
30
+ },
31
+
32
+ /// List all security rules evaluated by cf-zone-auditor
33
+ Rules {
34
+ /// Format for displaying rules (table or json)
35
+ #[arg(short, long, value_enum, default_value = "table")]
36
+ format: RuleListFormat,
37
+ },
38
+ }
39
+
40
+ #[derive(Debug, Args, Clone)]
41
+ pub struct AuditArgs {
42
+ /// Cloudflare API Token (can also be supplied via CF_API_TOKEN environment variable)
43
+ #[arg(short, long, env = "CF_API_TOKEN")]
44
+ pub token: Option<String>,
45
+
46
+ /// Cloudflare Account ID to filter zones
47
+ #[arg(short = 'a', long, env = "CF_ACCOUNT_ID")]
48
+ pub account_id: Option<String>,
49
+
50
+ /// Filter audit to a specific zone by domain name or 32-character zone ID
51
+ #[arg(short = 'z', long)]
52
+ pub zone: Option<String>,
53
+
54
+ /// Run audit offline using built-in synthetic test zones
55
+ #[arg(short = 'm', long)]
56
+ pub mock: bool,
57
+
58
+ /// Load zone data from a mock JSON file instead of live API
59
+ #[arg(short = 'i', long)]
60
+ pub input: Option<PathBuf>,
61
+
62
+ /// Output report format
63
+ #[arg(short = 'f', long, value_enum, default_value = "table")]
64
+ pub format: OutputFormat,
65
+
66
+ /// Write report to a file path instead of stdout
67
+ #[arg(short = 'o', long)]
68
+ pub output: Option<PathBuf>,
69
+
70
+ /// CI Compliance Gate: Exit with code 1 if average score is below threshold (0-100)
71
+ #[arg(long)]
72
+ pub min_score: Option<u32>,
73
+
74
+ /// CI Compliance Gate: Exit with code 1 if any Critical severity finding is found
75
+ #[arg(long)]
76
+ pub fail_on_critical: bool,
77
+
78
+ /// CI Compliance Gate: Exit with code 1 if any High or Critical severity finding is found
79
+ #[arg(long)]
80
+ pub fail_on_high: bool,
81
+
82
+ /// CI Compliance Gate: Shorthand for --fail-on-critical --fail-on-high --min-score 80
83
+ #[arg(long)]
84
+ pub check: bool,
85
+
86
+ /// Verbose output (includes passed check details)
87
+ #[arg(short = 'v', long)]
88
+ pub verbose: bool,
89
+
90
+ /// Quiet mode (suppresses banners and extraneous output)
91
+ #[arg(short = 'q', long)]
92
+ pub quiet: bool,
93
+
94
+ /// Disable colored terminal output
95
+ #[arg(long)]
96
+ pub no_color: bool,
97
+ }
98
+
99
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
100
+ pub enum OutputFormat {
101
+ Table,
102
+ Json,
103
+ Html,
104
+ Sarif,
105
+ }
106
+
107
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
108
+ pub enum RuleListFormat {
109
+ Table,
110
+ Json,
111
+ }
@@ -0,0 +1,405 @@
1
+ use crate::zone::client::provider::ZoneDataProvider;
2
+ use crate::zone::models::{
3
+ ApiResponse, BotManagementSetting, DnssecSetting, HstsSetting, IpAccessRule,
4
+ IpAccessRulesSetting, RateLimitRule, RateLimitSetting, RulesetInfo, SecurityHeaderSetting,
5
+ SettingItem, WafPackage, WafSetting, Zone, ZoneAuditData, ZoneLockdownRule,
6
+ ZoneLockdownSetting, ZoneSettings,
7
+ };
8
+ use anyhow::{Context, Result, bail};
9
+ use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
10
+ use reqwest::{Client, Response, StatusCode};
11
+ use std::future::Future;
12
+ use std::pin::Pin;
13
+ use std::time::Duration;
14
+
15
+ pub struct CloudflareClient {
16
+ client: Client,
17
+ base_url: String,
18
+ account_id: Option<String>,
19
+ }
20
+
21
+ impl CloudflareClient {
22
+ pub fn new(token: &str, account_id: Option<String>) -> Result<Self> {
23
+ let mut headers = HeaderMap::new();
24
+ let mut auth_val = HeaderValue::from_str(&format!("Bearer {}", token.trim()))
25
+ .context("Invalid API token header format")?;
26
+ auth_val.set_sensitive(true);
27
+ headers.insert(AUTHORIZATION, auth_val);
28
+ headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
29
+ headers.insert(
30
+ USER_AGENT,
31
+ HeaderValue::from_static("cf-zone-auditor/0.1.0 (Antigravity Security Auditor)"),
32
+ );
33
+
34
+ let client = Client::builder()
35
+ .default_headers(headers)
36
+ .timeout(Duration::from_secs(30))
37
+ .build()
38
+ .context("Failed to build HTTP client")?;
39
+
40
+ Ok(Self {
41
+ client,
42
+ base_url: "https://api.cloudflare.com/client/v4".to_string(),
43
+ account_id,
44
+ })
45
+ }
46
+
47
+ #[allow(dead_code)]
48
+ pub fn with_base_url(mut self, base_url: String) -> Self {
49
+ self.base_url = base_url;
50
+ self
51
+ }
52
+
53
+ /// Executes an HTTP GET request with automatic retry on 429 Too Many Requests
54
+ async fn get_with_retry(&self, url: &str) -> Result<Response> {
55
+ let mut retries = 0;
56
+ let max_retries = 3;
57
+
58
+ loop {
59
+ let resp = self.client.get(url).send().await?;
60
+
61
+ if resp.status() == StatusCode::TOO_MANY_REQUESTS && retries < max_retries {
62
+ retries += 1;
63
+ let wait_secs = resp
64
+ .headers()
65
+ .get("Retry-After")
66
+ .and_then(|h| h.to_str().ok())
67
+ .and_then(|s| s.parse::<u64>().ok())
68
+ .unwrap_or(2u64.pow(retries as u32));
69
+
70
+ tokio::time::sleep(Duration::from_secs(wait_secs)).await;
71
+ continue;
72
+ }
73
+
74
+ return Ok(resp);
75
+ }
76
+ }
77
+
78
+ /// Fetch all zones with pagination and optional filter
79
+ pub async fn fetch_zones_list(&self, zone_filter: Option<&str>) -> Result<Vec<Zone>> {
80
+ let mut zones = Vec::new();
81
+ let mut page = 1;
82
+ let per_page = 50;
83
+
84
+ loop {
85
+ let mut url = format!(
86
+ "{}/zones?page={}&per_page={}",
87
+ self.base_url, page, per_page
88
+ );
89
+
90
+ if let Some(ref acc) = self.account_id {
91
+ url.push_str(&format!("&account.id={}", acc));
92
+ }
93
+
94
+ if let Some(filter) = zone_filter
95
+ && !filter.is_empty() {
96
+ // Check if filter looks like a domain name vs zone ID (32 hex characters)
97
+ if filter.len() == 32 && filter.chars().all(|c| c.is_ascii_hexdigit()) {
98
+ // Zone ID
99
+ let direct_url = format!("{}/zones/{}", self.base_url, filter);
100
+ let resp = self.get_with_retry(&direct_url).await?;
101
+ if !resp.status().is_success() {
102
+ let text = resp.text().await.unwrap_or_default();
103
+ bail!("Failed to fetch zone {}: {}", filter, text);
104
+ }
105
+ let envelope: ApiResponse<Zone> = resp.json().await?;
106
+ if let Some(z) = envelope.result {
107
+ return Ok(vec![z]);
108
+ } else {
109
+ return Ok(vec![]);
110
+ }
111
+ } else {
112
+ url.push_str(&format!("&name={}", filter));
113
+ }
114
+ }
115
+
116
+ let resp = self.get_with_retry(&url).await?;
117
+ if !resp.status().is_success() {
118
+ let status = resp.status();
119
+ let text = resp.text().await.unwrap_or_default();
120
+ bail!("Cloudflare API error (HTTP {}): {}", status, text);
121
+ }
122
+
123
+ let envelope: ApiResponse<Vec<Zone>> = resp.json().await?;
124
+ if !envelope.success {
125
+ let errs = envelope
126
+ .errors
127
+ .unwrap_or_default()
128
+ .into_iter()
129
+ .map(|e| e.message)
130
+ .collect::<Vec<_>>()
131
+ .join(", ");
132
+ bail!("Cloudflare API returned error: {}", errs);
133
+ }
134
+
135
+ let page_zones = envelope.result.unwrap_or_default();
136
+ let count = page_zones.len();
137
+ zones.extend(page_zones);
138
+
139
+ if let Some(info) = envelope.result_info {
140
+ let total_pages = info.total_pages.unwrap_or(1);
141
+ if page >= total_pages || count == 0 {
142
+ break;
143
+ }
144
+ } else if count < per_page {
145
+ break;
146
+ }
147
+
148
+ page += 1;
149
+ }
150
+
151
+ Ok(zones)
152
+ }
153
+
154
+ /// Fetch all settings for a specific zone
155
+ pub async fn fetch_zone_settings(&self, zone_id: &str) -> Result<ZoneSettings> {
156
+ let url = format!("{}/zones/{}/settings", self.base_url, zone_id);
157
+ let resp = self.get_with_retry(&url).await?;
158
+
159
+ if !resp.status().is_success() {
160
+ return Ok(ZoneSettings::default());
161
+ }
162
+
163
+ let envelope: ApiResponse<Vec<SettingItem>> = resp.json().await.unwrap_or(ApiResponse {
164
+ success: false,
165
+ errors: None,
166
+ messages: None,
167
+ result: None,
168
+ result_info: None,
169
+ });
170
+
171
+ let mut settings = ZoneSettings::default();
172
+ if let Some(items) = envelope.result {
173
+ for item in items {
174
+ match item.id.as_str() {
175
+ "ssl" => {
176
+ if let Some(v) = item.value.as_str() {
177
+ settings.ssl = Some(v.to_string());
178
+ }
179
+ }
180
+ "min_tls_version" => {
181
+ if let Some(v) = item.value.as_str() {
182
+ settings.min_tls_version = Some(v.to_string());
183
+ }
184
+ }
185
+ "tls_1_3" => {
186
+ if let Some(v) = item.value.as_str() {
187
+ settings.tls_1_3 = Some(v.to_string());
188
+ }
189
+ }
190
+ "always_use_https" => {
191
+ if let Some(v) = item.value.as_str() {
192
+ settings.always_use_https = Some(v.to_string());
193
+ }
194
+ }
195
+ "automatic_https_rewrites" => {
196
+ if let Some(v) = item.value.as_str() {
197
+ settings.automatic_https_rewrites = Some(v.to_string());
198
+ }
199
+ }
200
+ "opportunistic_encryption" => {
201
+ if let Some(v) = item.value.as_str() {
202
+ settings.opportunistic_encryption = Some(v.to_string());
203
+ }
204
+ }
205
+ "security_header" => {
206
+ if let Ok(sh) =
207
+ serde_json::from_value::<SecurityHeaderSetting>(item.value.clone())
208
+ {
209
+ settings.security_header = Some(sh);
210
+ } else if let Some(obj) = item.value.as_object()
211
+ && let Some(sts) = obj.get("strict_transport_security")
212
+ && let Ok(hsts) = serde_json::from_value::<HstsSetting>(sts.clone())
213
+ {
214
+ settings.security_header = Some(SecurityHeaderSetting {
215
+ strict_transport_security: Some(hsts),
216
+ });
217
+ }
218
+ }
219
+ "security_level" => {
220
+ if let Some(v) = item.value.as_str() {
221
+ settings.security_level = Some(v.to_string());
222
+ }
223
+ }
224
+ "browser_check" => {
225
+ if let Some(v) = item.value.as_str() {
226
+ settings.browser_check = Some(v.to_string());
227
+ }
228
+ }
229
+ "challenge_ttl" => {
230
+ if let Some(v) = item.value.as_i64() {
231
+ settings.challenge_ttl = Some(v);
232
+ }
233
+ }
234
+ "brotli" => {
235
+ if let Some(v) = item.value.as_str() {
236
+ settings.brotli = Some(v.to_string());
237
+ }
238
+ }
239
+ "early_hints" => {
240
+ if let Some(v) = item.value.as_str() {
241
+ settings.early_hints = Some(v.to_string());
242
+ }
243
+ }
244
+ _ => {}
245
+ }
246
+ }
247
+ }
248
+
249
+ Ok(settings)
250
+ }
251
+
252
+ /// Fetch DNSSEC configuration for a zone
253
+ pub async fn fetch_dnssec(&self, zone_id: &str) -> Result<DnssecSetting> {
254
+ let url = format!("{}/zones/{}/dnssec", self.base_url, zone_id);
255
+ let resp = self.get_with_retry(&url).await?;
256
+
257
+ if !resp.status().is_success() {
258
+ return Ok(DnssecSetting::default());
259
+ }
260
+
261
+ let envelope: ApiResponse<DnssecSetting> = resp.json().await.unwrap_or(ApiResponse {
262
+ success: false,
263
+ errors: None,
264
+ messages: None,
265
+ result: None,
266
+ result_info: None,
267
+ });
268
+
269
+ Ok(envelope.result.unwrap_or_default())
270
+ }
271
+
272
+ /// Fetch WAF configuration (Packages and Modern Rulesets)
273
+ pub async fn fetch_waf(&self, zone_id: &str) -> Result<WafSetting> {
274
+ let mut waf = WafSetting::default();
275
+
276
+ // 1. Check WAF packages
277
+ let pkg_url = format!("{}/zones/{}/firewall/waf/packages", self.base_url, zone_id);
278
+ if let Ok(resp) = self.get_with_retry(&pkg_url).await
279
+ && resp.status().is_success()
280
+ && let Ok(envelope) = resp.json::<ApiResponse<Vec<WafPackage>>>().await
281
+ && let Some(pkgs) = envelope.result
282
+ && !pkgs.is_empty() {
283
+ waf.waf_enabled = true;
284
+ waf.managed_rules_active = true;
285
+ waf.packages = pkgs;
286
+ }
287
+
288
+ // 2. Check Modern Rulesets
289
+ let ruleset_url = format!("{}/zones/{}/rulesets", self.base_url, zone_id);
290
+ if let Ok(resp) = self.get_with_retry(&ruleset_url).await
291
+ && resp.status().is_success()
292
+ && let Ok(envelope) = resp.json::<ApiResponse<Vec<RulesetInfo>>>().await
293
+ && let Some(rulesets) = envelope.result
294
+ && !rulesets.is_empty() {
295
+ waf.waf_enabled = true;
296
+ waf.managed_rules_active = true;
297
+ waf.rulesets = rulesets;
298
+ }
299
+
300
+ Ok(waf)
301
+ }
302
+
303
+ /// Fetch Bot Management / Bot Fight Mode
304
+ pub async fn fetch_bot_management(&self, zone_id: &str) -> Result<BotManagementSetting> {
305
+ let url = format!("{}/zones/{}/bot_management", self.base_url, zone_id);
306
+ if let Ok(resp) = self.get_with_retry(&url).await
307
+ && resp.status().is_success()
308
+ && let Ok(envelope) = resp.json::<ApiResponse<BotManagementSetting>>().await
309
+ && let Some(bot) = envelope.result {
310
+ return Ok(bot);
311
+ }
312
+ Ok(BotManagementSetting::default())
313
+ }
314
+
315
+ /// Fetch Rate Limiting rules
316
+ pub async fn fetch_rate_limits(&self, zone_id: &str) -> Result<RateLimitSetting> {
317
+ let url = format!("{}/zones/{}/rate_limits", self.base_url, zone_id);
318
+ if let Ok(resp) = self.get_with_retry(&url).await
319
+ && resp.status().is_success()
320
+ && let Ok(envelope) = resp.json::<ApiResponse<Vec<RateLimitRule>>>().await {
321
+ return Ok(RateLimitSetting {
322
+ rules: envelope.result.unwrap_or_default(),
323
+ });
324
+ }
325
+ Ok(RateLimitSetting::default())
326
+ }
327
+
328
+ /// Fetch Zone Lockdown rules
329
+ pub async fn fetch_lockdowns(&self, zone_id: &str) -> Result<ZoneLockdownSetting> {
330
+ let url = format!("{}/zones/{}/firewall/lockdowns", self.base_url, zone_id);
331
+ if let Ok(resp) = self.get_with_retry(&url).await
332
+ && resp.status().is_success()
333
+ && let Ok(envelope) = resp.json::<ApiResponse<Vec<ZoneLockdownRule>>>().await {
334
+ return Ok(ZoneLockdownSetting {
335
+ rules: envelope.result.unwrap_or_default(),
336
+ });
337
+ }
338
+ Ok(ZoneLockdownSetting::default())
339
+ }
340
+
341
+ /// Fetch IP Access Rules
342
+ pub async fn fetch_ip_access_rules(&self, zone_id: &str) -> Result<IpAccessRulesSetting> {
343
+ let url = format!(
344
+ "{}/zones/{}/firewall/access_rules/rules",
345
+ self.base_url, zone_id
346
+ );
347
+ if let Ok(resp) = self.get_with_retry(&url).await
348
+ && resp.status().is_success()
349
+ && let Ok(envelope) = resp.json::<ApiResponse<Vec<IpAccessRule>>>().await {
350
+ return Ok(IpAccessRulesSetting {
351
+ rules: envelope.result.unwrap_or_default(),
352
+ });
353
+ }
354
+ Ok(IpAccessRulesSetting::default())
355
+ }
356
+
357
+ /// Fetches all audit data for a single zone concurrently
358
+ pub async fn fetch_zone_audit_data(&self, zone: Zone) -> Result<ZoneAuditData> {
359
+ let zone_id = zone.id.clone();
360
+
361
+ let (settings_res, dnssec_res, waf_res, bot_res, rate_res, lock_res, ip_res) = tokio::join!(
362
+ self.fetch_zone_settings(&zone_id),
363
+ self.fetch_dnssec(&zone_id),
364
+ self.fetch_waf(&zone_id),
365
+ self.fetch_bot_management(&zone_id),
366
+ self.fetch_rate_limits(&zone_id),
367
+ self.fetch_lockdowns(&zone_id),
368
+ self.fetch_ip_access_rules(&zone_id),
369
+ );
370
+
371
+ Ok(ZoneAuditData {
372
+ zone,
373
+ settings: settings_res.unwrap_or_default(),
374
+ dnssec: dnssec_res.unwrap_or_default(),
375
+ waf: waf_res.unwrap_or_default(),
376
+ bot_management: bot_res.unwrap_or_default(),
377
+ rate_limits: rate_res.unwrap_or_default(),
378
+ lockdowns: lock_res.unwrap_or_default(),
379
+ ip_access_rules: ip_res.unwrap_or_default(),
380
+ })
381
+ }
382
+ }
383
+
384
+ impl ZoneDataProvider for CloudflareClient {
385
+ fn fetch_all_zones<'a>(
386
+ &'a self,
387
+ zone_filter: Option<&'a str>,
388
+ ) -> Pin<Box<dyn Future<Output = Result<Vec<ZoneAuditData>>> + Send + 'a>> {
389
+ Box::pin(async move {
390
+ let zones = self.fetch_zones_list(zone_filter).await?;
391
+
392
+ let mut tasks = Vec::new();
393
+ for zone in zones {
394
+ tasks.push(self.fetch_zone_audit_data(zone));
395
+ }
396
+
397
+ let mut results = Vec::with_capacity(tasks.len());
398
+ for task in tasks {
399
+ results.push(task.await?);
400
+ }
401
+
402
+ Ok(results)
403
+ })
404
+ }
405
+ }
@@ -0,0 +1,5 @@
1
+ pub mod cf_client;
2
+ pub mod provider;
3
+
4
+ pub use cf_client::CloudflareClient;
5
+ pub use provider::ZoneDataProvider;
@@ -0,0 +1,12 @@
1
+ use crate::zone::models::ZoneAuditData;
2
+ use anyhow::Result;
3
+ use std::future::Future;
4
+ use std::pin::Pin;
5
+
6
+ /// Trait abstracting the data source for Cloudflare zones
7
+ pub trait ZoneDataProvider: Send + Sync {
8
+ fn fetch_all_zones<'a>(
9
+ &'a self,
10
+ zone_filter: Option<&'a str>,
11
+ ) -> Pin<Box<dyn Future<Output = Result<Vec<ZoneAuditData>>> + Send + 'a>>;
12
+ }