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
package/src/zone/cli.rs
ADDED
|
@@ -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,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
|
+
}
|