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,320 @@
1
+ use crate::origin::confidence::calculate_confidence;
2
+ use crate::origin::models::{
3
+ CandidateIp, ConfidenceLevel, DiscoverySource, HunterFinding, ProbeMatchDetails, ProbeResult,
4
+ ScanReport, ScanSummary, TargetBaseline,
5
+ };
6
+ use crate::origin::remediation::generate_remediation_plan;
7
+ use chrono::Utc;
8
+ use std::collections::HashMap;
9
+ use std::net::IpAddr;
10
+
11
+ /// Generates a synthetic mock scan report for demonstration and offline testing
12
+ pub fn run_mock_scan(domain: &str) -> ScanReport {
13
+ let target = if domain.trim().is_empty() {
14
+ "example-corp.com"
15
+ } else {
16
+ domain.trim()
17
+ };
18
+
19
+ let cf_edge1: IpAddr = "104.21.55.10".parse().unwrap();
20
+ let cf_edge2: IpAddr = "172.67.140.22".parse().unwrap();
21
+ let baseline_hash = "d5a8b73f9e2c4a1b8e6f7d0c3a5e9b1f2e4d6c8a0b3e5f7a9c1d3e5f7a9c1d3e";
22
+ let baseline_title = format!("{} | Secure Enterprise Gateway", target);
23
+
24
+ let mut baseline_headers = HashMap::new();
25
+ baseline_headers.insert("server".to_string(), "cloudflare".to_string());
26
+ baseline_headers.insert("cf-ray".to_string(), "8df4567890abcdef-ORD".to_string());
27
+ baseline_headers.insert("cf-cache-status".to_string(), "HIT".to_string());
28
+ baseline_headers.insert(
29
+ "content-type".to_string(),
30
+ "text/html; charset=UTF-8".to_string(),
31
+ );
32
+
33
+ let baseline = TargetBaseline {
34
+ domain: target.to_string(),
35
+ resolved_ips: vec![cf_edge1, cf_edge2],
36
+ is_behind_cloudflare: true,
37
+ cloudflare_ips: vec![cf_edge1, cf_edge2],
38
+ non_cloudflare_ips: vec![],
39
+ http_status: Some(200),
40
+ html_title: Some(baseline_title.clone()),
41
+ body_sha256: Some(baseline_hash.to_string()),
42
+ body_length: 4520,
43
+ server_header: Some("cloudflare".to_string()),
44
+ headers: baseline_headers,
45
+ };
46
+
47
+ // 1. Confirmed Origin: direct/origin subdomain
48
+ let origin_ip: IpAddr = "198.51.100.42".parse().unwrap();
49
+ let candidate1 = CandidateIp {
50
+ ip: origin_ip,
51
+ source: DiscoverySource::Subdomain(format!("origin.{}", target)),
52
+ hostname: Some(format!("origin.{}", target)),
53
+ is_cloudflare: false,
54
+ notes: vec![format!(
55
+ "Direct bypass subdomain origin.{} resolved to unmasked IP",
56
+ target
57
+ )],
58
+ };
59
+
60
+ let mut probe1_headers = HashMap::new();
61
+ probe1_headers.insert("server".to_string(), "nginx/1.24.0 (Ubuntu)".to_string());
62
+ probe1_headers.insert("x-powered-by".to_string(), "PHP/8.3".to_string());
63
+
64
+ let probe1 = ProbeResult {
65
+ ip: origin_ip,
66
+ port: 443,
67
+ protocol: "https".to_string(),
68
+ url: format!("https://{}:443/", origin_ip),
69
+ success: true,
70
+ status_code: Some(200),
71
+ html_title: Some(baseline_title.clone()),
72
+ body_sha256: Some(baseline_hash.to_string()),
73
+ body_length: 4520,
74
+ server_header: Some("nginx/1.24.0 (Ubuntu)".to_string()),
75
+ headers: probe1_headers,
76
+ response_time_ms: 32,
77
+ error: None,
78
+ match_details: Some(ProbeMatchDetails {
79
+ exact_body_hash_match: true,
80
+ title_match: true,
81
+ status_code_match: true,
82
+ body_length_delta: 0,
83
+ header_similarity_score: 0.95,
84
+ cf_ray_present: false,
85
+ direct_server_header: Some("nginx/1.24.0 (Ubuntu)".to_string()),
86
+ baseline_server_header: Some("cloudflare".to_string()),
87
+ }),
88
+ };
89
+
90
+ let (conf1_lvl, conf1_score, conf1_reason) =
91
+ calculate_confidence(&baseline, &candidate1.source, std::slice::from_ref(&probe1), &[]);
92
+
93
+ let finding1 = HunterFinding {
94
+ candidate_ip: origin_ip,
95
+ hostname: candidate1.hostname.clone(),
96
+ discovery_source: candidate1.source.clone(),
97
+ confidence: conf1_lvl,
98
+ confidence_score: conf1_score,
99
+ confidence_reason: conf1_reason,
100
+ successful_probes: vec![probe1],
101
+ failed_probes: vec![],
102
+ is_origin_confirmed: conf1_lvl == ConfidenceLevel::Confirmed,
103
+ };
104
+
105
+ // 2. High Confidence: Certificate Transparency Dev SAN
106
+ let dev_ip: IpAddr = "198.51.100.44".parse().unwrap();
107
+ let candidate2 = CandidateIp {
108
+ ip: dev_ip,
109
+ source: DiscoverySource::CertificateTransparency(format!("dev-backend.{}", target)),
110
+ hostname: Some(format!("dev-backend.{}", target)),
111
+ is_cloudflare: false,
112
+ notes: vec![format!(
113
+ "Historical crt.sh SAN dev-backend.{} resolved to unmasked IP",
114
+ target
115
+ )],
116
+ };
117
+
118
+ let mut probe2_headers = HashMap::new();
119
+ probe2_headers.insert("server".to_string(), "Apache/2.4.52 (Ubuntu)".to_string());
120
+
121
+ let probe2 = ProbeResult {
122
+ ip: dev_ip,
123
+ port: 443,
124
+ protocol: "https".to_string(),
125
+ url: format!("https://{}:443/", dev_ip),
126
+ success: true,
127
+ status_code: Some(200),
128
+ html_title: Some(baseline_title.clone()),
129
+ body_sha256: Some(
130
+ "e7b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1".to_string(),
131
+ ),
132
+ body_length: 4610,
133
+ server_header: Some("Apache/2.4.52 (Ubuntu)".to_string()),
134
+ headers: probe2_headers,
135
+ response_time_ms: 48,
136
+ error: None,
137
+ match_details: Some(ProbeMatchDetails {
138
+ exact_body_hash_match: false,
139
+ title_match: true,
140
+ status_code_match: true,
141
+ body_length_delta: 90,
142
+ header_similarity_score: 0.85,
143
+ cf_ray_present: false,
144
+ direct_server_header: Some("Apache/2.4.52 (Ubuntu)".to_string()),
145
+ baseline_server_header: Some("cloudflare".to_string()),
146
+ }),
147
+ };
148
+
149
+ let (conf2_lvl, conf2_score, conf2_reason) =
150
+ calculate_confidence(&baseline, &candidate2.source, std::slice::from_ref(&probe2), &[]);
151
+
152
+ let finding2 = HunterFinding {
153
+ candidate_ip: dev_ip,
154
+ hostname: candidate2.hostname.clone(),
155
+ discovery_source: candidate2.source.clone(),
156
+ confidence: conf2_lvl,
157
+ confidence_score: conf2_score,
158
+ confidence_reason: conf2_reason,
159
+ successful_probes: vec![probe2],
160
+ failed_probes: vec![],
161
+ is_origin_confirmed: conf2_lvl == ConfidenceLevel::Confirmed,
162
+ };
163
+
164
+ // 3. Medium Confidence: Mail Server (MX)
165
+ let mail_ip: IpAddr = "198.51.100.43".parse().unwrap();
166
+ let candidate3 = CandidateIp {
167
+ ip: mail_ip,
168
+ source: DiscoverySource::MxRecord(format!("mail.{}", target)),
169
+ hostname: Some(format!("mail.{}", target)),
170
+ is_cloudflare: false,
171
+ notes: vec![format!(
172
+ "MX record points to mail.{} on non-Cloudflare IP",
173
+ target
174
+ )],
175
+ };
176
+
177
+ let probe3 = ProbeResult {
178
+ ip: mail_ip,
179
+ port: 80,
180
+ protocol: "http".to_string(),
181
+ url: format!("http://{}:80/", mail_ip),
182
+ success: true,
183
+ status_code: Some(301),
184
+ html_title: Some("Webmail Login".to_string()),
185
+ body_sha256: Some(
186
+ "f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2".to_string(),
187
+ ),
188
+ body_length: 512,
189
+ server_header: Some("cPanel Web Services".to_string()),
190
+ headers: HashMap::new(),
191
+ response_time_ms: 60,
192
+ error: None,
193
+ match_details: Some(ProbeMatchDetails {
194
+ exact_body_hash_match: false,
195
+ title_match: false,
196
+ status_code_match: false,
197
+ body_length_delta: -4008,
198
+ header_similarity_score: 0.3,
199
+ cf_ray_present: false,
200
+ direct_server_header: Some("cPanel Web Services".to_string()),
201
+ baseline_server_header: Some("cloudflare".to_string()),
202
+ }),
203
+ };
204
+
205
+ let (conf3_lvl, conf3_score, conf3_reason) =
206
+ calculate_confidence(&baseline, &candidate3.source, std::slice::from_ref(&probe3), &[]);
207
+
208
+ let finding3 = HunterFinding {
209
+ candidate_ip: mail_ip,
210
+ hostname: candidate3.hostname.clone(),
211
+ discovery_source: candidate3.source.clone(),
212
+ confidence: conf3_lvl,
213
+ confidence_score: conf3_score,
214
+ confidence_reason: conf3_reason,
215
+ successful_probes: vec![probe3],
216
+ failed_probes: vec![],
217
+ is_origin_confirmed: conf3_lvl == ConfidenceLevel::Confirmed,
218
+ };
219
+
220
+ // 4. Medium Confidence: SPF IP (Firewalled)
221
+ let spf_ip: IpAddr = "203.0.113.10".parse().unwrap();
222
+ let candidate4 = CandidateIp {
223
+ ip: spf_ip,
224
+ source: DiscoverySource::SpfRecord(format!(
225
+ "v=spf1 ip4:{} include:_spf.google.com ~all",
226
+ spf_ip
227
+ )),
228
+ hostname: None,
229
+ is_cloudflare: false,
230
+ notes: vec!["Extracted from SPF policy declaration".into()],
231
+ };
232
+
233
+ let failed_probe = ProbeResult {
234
+ ip: spf_ip,
235
+ port: 443,
236
+ protocol: "https".to_string(),
237
+ url: format!("https://{}:443/", spf_ip),
238
+ success: false,
239
+ status_code: None,
240
+ html_title: None,
241
+ body_sha256: None,
242
+ body_length: 0,
243
+ server_header: None,
244
+ headers: HashMap::new(),
245
+ response_time_ms: 2000,
246
+ error: Some("Connection timed out (no route to host / firewalled)".to_string()),
247
+ match_details: None,
248
+ };
249
+
250
+ let (conf4_lvl, conf4_score, conf4_reason) =
251
+ calculate_confidence(&baseline, &candidate4.source, &[], std::slice::from_ref(&failed_probe));
252
+
253
+ let finding4 = HunterFinding {
254
+ candidate_ip: spf_ip,
255
+ hostname: candidate4.hostname.clone(),
256
+ discovery_source: candidate4.source.clone(),
257
+ confidence: conf4_lvl,
258
+ confidence_score: conf4_score,
259
+ confidence_reason: conf4_reason,
260
+ successful_probes: vec![],
261
+ failed_probes: vec![failed_probe],
262
+ is_origin_confirmed: conf4_lvl == ConfidenceLevel::Confirmed,
263
+ };
264
+
265
+ let findings = vec![finding1, finding2, finding3, finding4];
266
+ let candidates = vec![candidate1, candidate2, candidate3, candidate4];
267
+
268
+ let summary = ScanSummary {
269
+ target_domain: target.to_string(),
270
+ scanned_at: Utc::now(),
271
+ duration_seconds: 1.42,
272
+ is_behind_cloudflare: true,
273
+ cloudflare_edge_ips: vec![cf_edge1, cf_edge2],
274
+ candidates_discovered: candidates.len(),
275
+ origins_confirmed: findings
276
+ .iter()
277
+ .filter(|f| f.confidence == ConfidenceLevel::Confirmed)
278
+ .count(),
279
+ high_confidence_origins: findings
280
+ .iter()
281
+ .filter(|f| f.confidence == ConfidenceLevel::High)
282
+ .count(),
283
+ medium_confidence_origins: findings
284
+ .iter()
285
+ .filter(|f| f.confidence == ConfidenceLevel::Medium)
286
+ .count(),
287
+ low_confidence_origins: findings
288
+ .iter()
289
+ .filter(|f| f.confidence == ConfidenceLevel::Low)
290
+ .count(),
291
+ is_origin_leaked: true,
292
+ };
293
+
294
+ let remediation = generate_remediation_plan();
295
+
296
+ ScanReport {
297
+ summary,
298
+ baseline,
299
+ findings,
300
+ candidates,
301
+ remediation,
302
+ }
303
+ }
304
+
305
+ #[cfg(test)]
306
+ mod tests {
307
+ use super::*;
308
+
309
+ #[test]
310
+ fn test_mock_scan_generation() {
311
+ let report = run_mock_scan("test-target.com");
312
+ assert_eq!(report.summary.target_domain, "test-target.com");
313
+ assert!(report.summary.is_behind_cloudflare);
314
+ assert_eq!(report.findings.len(), 4);
315
+ assert_eq!(report.summary.origins_confirmed, 1);
316
+ assert_eq!(report.summary.high_confidence_origins, 1);
317
+ assert_eq!(report.summary.medium_confidence_origins, 2);
318
+ assert!(report.summary.is_origin_leaked);
319
+ }
320
+ }
@@ -0,0 +1,19 @@
1
+ pub mod cli;
2
+ pub mod cloudflare;
3
+ pub mod confidence;
4
+ pub mod crtsh;
5
+ pub mod dns;
6
+ pub mod enumerator;
7
+ pub mod error;
8
+ pub mod mock;
9
+ pub mod models;
10
+ pub mod prober;
11
+ pub mod remediation;
12
+ pub mod report;
13
+ pub mod scanner;
14
+
15
+ pub use cli::Cli;
16
+ pub use error::{HunterError, Result};
17
+ pub use models::{ConfidenceLevel, DiscoverySource, HunterFinding, ScanReport, ScanSummary};
18
+ pub use report::OutputFormat;
19
+ pub use scanner::{ScanOptions, run_scan};
@@ -0,0 +1,167 @@
1
+ use chrono::{DateTime, Utc};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::collections::HashMap;
4
+ use std::net::IpAddr;
5
+
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
7
+ #[serde(rename_all = "UPPERCASE")]
8
+ pub enum ConfidenceLevel {
9
+ Low = 1,
10
+ Medium = 2,
11
+ High = 3,
12
+ Confirmed = 4,
13
+ }
14
+
15
+ impl std::fmt::Display for ConfidenceLevel {
16
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17
+ match self {
18
+ ConfidenceLevel::Confirmed => write!(f, "CONFIRMED"),
19
+ ConfidenceLevel::High => write!(f, "HIGH"),
20
+ ConfidenceLevel::Medium => write!(f, "MEDIUM"),
21
+ ConfidenceLevel::Low => write!(f, "LOW"),
22
+ }
23
+ }
24
+ }
25
+
26
+ impl std::str::FromStr for ConfidenceLevel {
27
+ type Err = String;
28
+
29
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
30
+ match s.to_uppercase().as_str() {
31
+ "CONFIRMED" | "100" => Ok(ConfidenceLevel::Confirmed),
32
+ "HIGH" | "85" => Ok(ConfidenceLevel::High),
33
+ "MEDIUM" | "60" => Ok(ConfidenceLevel::Medium),
34
+ "LOW" | "30" => Ok(ConfidenceLevel::Low),
35
+ _ => Err(format!("Unknown confidence level: {}", s)),
36
+ }
37
+ }
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize)]
41
+ pub struct TargetBaseline {
42
+ pub domain: String,
43
+ pub resolved_ips: Vec<IpAddr>,
44
+ pub is_behind_cloudflare: bool,
45
+ pub cloudflare_ips: Vec<IpAddr>,
46
+ pub non_cloudflare_ips: Vec<IpAddr>,
47
+ pub http_status: Option<u16>,
48
+ pub html_title: Option<String>,
49
+ pub body_sha256: Option<String>,
50
+ pub body_length: usize,
51
+ pub server_header: Option<String>,
52
+ pub headers: HashMap<String, String>,
53
+ }
54
+
55
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
56
+ #[serde(rename_all = "snake_case")]
57
+ pub enum DiscoverySource {
58
+ Subdomain(String),
59
+ MxRecord(String),
60
+ SpfRecord(String),
61
+ TxtRecord(String),
62
+ CertificateTransparency(String),
63
+ HistoricalDns(String),
64
+ DirectDns(String),
65
+ Custom(String),
66
+ }
67
+
68
+ impl std::fmt::Display for DiscoverySource {
69
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70
+ match self {
71
+ DiscoverySource::Subdomain(sub) => write!(f, "Subdomain ({})", sub),
72
+ DiscoverySource::MxRecord(mx) => write!(f, "MX Record ({})", mx),
73
+ DiscoverySource::SpfRecord(spf) => write!(f, "SPF Record ({})", spf),
74
+ DiscoverySource::TxtRecord(txt) => write!(f, "TXT Record ({})", txt),
75
+ DiscoverySource::CertificateTransparency(san) => write!(f, "crt.sh SAN ({})", san),
76
+ DiscoverySource::HistoricalDns(src) => write!(f, "Historical DNS ({})", src),
77
+ DiscoverySource::DirectDns(rec) => write!(f, "Direct DNS ({})", rec),
78
+ DiscoverySource::Custom(desc) => write!(f, "Custom ({})", desc),
79
+ }
80
+ }
81
+ }
82
+
83
+ #[derive(Debug, Clone, Serialize, Deserialize)]
84
+ pub struct CandidateIp {
85
+ pub ip: IpAddr,
86
+ pub source: DiscoverySource,
87
+ pub hostname: Option<String>,
88
+ pub is_cloudflare: bool,
89
+ pub notes: Vec<String>,
90
+ }
91
+
92
+ #[derive(Debug, Clone, Serialize, Deserialize)]
93
+ pub struct ProbeMatchDetails {
94
+ pub exact_body_hash_match: bool,
95
+ pub title_match: bool,
96
+ pub status_code_match: bool,
97
+ pub body_length_delta: i64,
98
+ pub header_similarity_score: f32,
99
+ pub cf_ray_present: bool,
100
+ pub direct_server_header: Option<String>,
101
+ pub baseline_server_header: Option<String>,
102
+ }
103
+
104
+ #[derive(Debug, Clone, Serialize, Deserialize)]
105
+ pub struct ProbeResult {
106
+ pub ip: IpAddr,
107
+ pub port: u16,
108
+ pub protocol: String,
109
+ pub url: String,
110
+ pub success: bool,
111
+ pub status_code: Option<u16>,
112
+ pub html_title: Option<String>,
113
+ pub body_sha256: Option<String>,
114
+ pub body_length: usize,
115
+ pub server_header: Option<String>,
116
+ pub headers: HashMap<String, String>,
117
+ pub response_time_ms: u64,
118
+ pub error: Option<String>,
119
+ pub match_details: Option<ProbeMatchDetails>,
120
+ }
121
+
122
+ #[derive(Debug, Clone, Serialize, Deserialize)]
123
+ pub struct HunterFinding {
124
+ pub candidate_ip: IpAddr,
125
+ pub hostname: Option<String>,
126
+ pub discovery_source: DiscoverySource,
127
+ pub confidence: ConfidenceLevel,
128
+ pub confidence_score: u8, // 0 - 100
129
+ pub confidence_reason: String,
130
+ pub successful_probes: Vec<ProbeResult>,
131
+ pub failed_probes: Vec<ProbeResult>,
132
+ pub is_origin_confirmed: bool,
133
+ }
134
+
135
+ #[derive(Debug, Clone, Serialize, Deserialize)]
136
+ pub struct ScanSummary {
137
+ pub target_domain: String,
138
+ pub scanned_at: DateTime<Utc>,
139
+ pub duration_seconds: f64,
140
+ pub is_behind_cloudflare: bool,
141
+ pub cloudflare_edge_ips: Vec<IpAddr>,
142
+ pub candidates_discovered: usize,
143
+ pub origins_confirmed: usize,
144
+ pub high_confidence_origins: usize,
145
+ pub medium_confidence_origins: usize,
146
+ pub low_confidence_origins: usize,
147
+ pub is_origin_leaked: bool,
148
+ }
149
+
150
+ #[derive(Debug, Clone, Serialize, Deserialize)]
151
+ pub struct RemediationStep {
152
+ pub id: String,
153
+ pub title: String,
154
+ pub priority: String, // "CRITICAL", "HIGH", "MEDIUM"
155
+ pub description: String,
156
+ pub commands: Vec<String>,
157
+ pub doc_url: String,
158
+ }
159
+
160
+ #[derive(Debug, Clone, Serialize, Deserialize)]
161
+ pub struct ScanReport {
162
+ pub summary: ScanSummary,
163
+ pub baseline: TargetBaseline,
164
+ pub findings: Vec<HunterFinding>,
165
+ pub candidates: Vec<CandidateIp>,
166
+ pub remediation: Vec<RemediationStep>,
167
+ }