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,625 @@
|
|
|
1
|
+
use crate::origin::error::{HunterError, Result};
|
|
2
|
+
use crate::origin::models::{ConfidenceLevel, ScanReport};
|
|
3
|
+
use colored::Colorize;
|
|
4
|
+
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
|
|
5
|
+
use comfy_table::presets::UTF8_FULL;
|
|
6
|
+
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table};
|
|
7
|
+
use serde_json::json;
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
10
|
+
pub enum OutputFormat {
|
|
11
|
+
Text,
|
|
12
|
+
Json,
|
|
13
|
+
Sarif,
|
|
14
|
+
Html,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl std::str::FromStr for OutputFormat {
|
|
18
|
+
type Err = String;
|
|
19
|
+
|
|
20
|
+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
|
21
|
+
match s.to_lowercase().as_str() {
|
|
22
|
+
"text" | "term" | "terminal" | "table" => Ok(OutputFormat::Text),
|
|
23
|
+
"json" => Ok(OutputFormat::Json),
|
|
24
|
+
"sarif" => Ok(OutputFormat::Sarif),
|
|
25
|
+
"html" => Ok(OutputFormat::Html),
|
|
26
|
+
_ => Err(format!(
|
|
27
|
+
"Unsupported output format '{}'. Valid: text, json, sarif, html",
|
|
28
|
+
s
|
|
29
|
+
)),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Renders scan report in the requested output format
|
|
35
|
+
pub fn render_report(report: &ScanReport, format: OutputFormat) -> Result<String> {
|
|
36
|
+
match format {
|
|
37
|
+
OutputFormat::Text => Ok(render_text(report)),
|
|
38
|
+
OutputFormat::Json => Ok(render_json(report)?),
|
|
39
|
+
OutputFormat::Sarif => Ok(render_sarif(report)?),
|
|
40
|
+
OutputFormat::Html => Ok(render_html(report)),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Renders rich colored terminal report with comfy-table
|
|
45
|
+
pub fn render_text(report: &ScanReport) -> String {
|
|
46
|
+
let mut out = String::new();
|
|
47
|
+
|
|
48
|
+
out.push('\n');
|
|
49
|
+
out.push_str(
|
|
50
|
+
&"╔══════════════════════════════════════════════════════════════════════════════╗\n"
|
|
51
|
+
.bright_cyan()
|
|
52
|
+
.bold()
|
|
53
|
+
.to_string(),
|
|
54
|
+
);
|
|
55
|
+
out.push_str(
|
|
56
|
+
&"║ CLOUDFLARE ORIGIN HUNTER (v0.1.0) ║\n"
|
|
57
|
+
.bright_cyan()
|
|
58
|
+
.bold()
|
|
59
|
+
.to_string(),
|
|
60
|
+
);
|
|
61
|
+
out.push_str(
|
|
62
|
+
&"╚══════════════════════════════════════════════════════════════════════════════╝\n"
|
|
63
|
+
.bright_cyan()
|
|
64
|
+
.bold()
|
|
65
|
+
.to_string(),
|
|
66
|
+
);
|
|
67
|
+
out.push('\n');
|
|
68
|
+
|
|
69
|
+
out.push_str(&format!(
|
|
70
|
+
" 🎯 Target Domain: {}\n",
|
|
71
|
+
report.summary.target_domain.bright_yellow().bold()
|
|
72
|
+
));
|
|
73
|
+
out.push_str(&format!(
|
|
74
|
+
" 🕒 Scanned At: {}\n",
|
|
75
|
+
report
|
|
76
|
+
.summary
|
|
77
|
+
.scanned_at
|
|
78
|
+
.format("%Y-%m-%d %H:%M:%S UTC")
|
|
79
|
+
.to_string()
|
|
80
|
+
.cyan()
|
|
81
|
+
));
|
|
82
|
+
out.push_str(&format!(
|
|
83
|
+
" ⏱️ Scan Duration: {:.2}s\n",
|
|
84
|
+
report.summary.duration_seconds
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
let cf_status = if report.summary.is_behind_cloudflare {
|
|
88
|
+
"PROXIED BEHIND CLOUDFLARE".bright_green().bold()
|
|
89
|
+
} else {
|
|
90
|
+
"DIRECT / NOT CLOUDFLARE PROXIED".bright_red().bold()
|
|
91
|
+
};
|
|
92
|
+
out.push_str(&format!(" 🛡️ Cloudflare Edge: {}\n", cf_status));
|
|
93
|
+
|
|
94
|
+
let edge_ips_str = report
|
|
95
|
+
.summary
|
|
96
|
+
.cloudflare_edge_ips
|
|
97
|
+
.iter()
|
|
98
|
+
.map(|ip| ip.to_string())
|
|
99
|
+
.collect::<Vec<_>>()
|
|
100
|
+
.join(", ");
|
|
101
|
+
out.push_str(&format!(
|
|
102
|
+
" 🌐 Cloudflare Edge IPs: {}\n",
|
|
103
|
+
if edge_ips_str.is_empty() {
|
|
104
|
+
"None".dimmed().to_string()
|
|
105
|
+
} else {
|
|
106
|
+
edge_ips_str.cyan().to_string()
|
|
107
|
+
}
|
|
108
|
+
));
|
|
109
|
+
|
|
110
|
+
if let Some(ref title) = report.baseline.html_title {
|
|
111
|
+
out.push_str(&format!(" 📄 Baseline Title: {}\n", title.dimmed()));
|
|
112
|
+
}
|
|
113
|
+
if let Some(ref hash) = report.baseline.body_sha256 {
|
|
114
|
+
out.push_str(&format!(
|
|
115
|
+
" 🔑 Baseline SHA-256: {}\n",
|
|
116
|
+
hash[..16].dimmed()
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
out.push('\n');
|
|
121
|
+
out.push_str(
|
|
122
|
+
&"─── CANDIDATE ORIGIN IP FINDINGS ───────────────────────────────────────────────\n"
|
|
123
|
+
.bright_white()
|
|
124
|
+
.bold()
|
|
125
|
+
.to_string(),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if report.findings.is_empty() {
|
|
129
|
+
out.push_str(&format!(
|
|
130
|
+
" {}\n\n",
|
|
131
|
+
"✅ No unmasked origin IP addresses detected. Target is well-protected."
|
|
132
|
+
.bright_green()
|
|
133
|
+
.bold()
|
|
134
|
+
));
|
|
135
|
+
} else {
|
|
136
|
+
let mut table = Table::new();
|
|
137
|
+
table.load_preset(UTF8_FULL);
|
|
138
|
+
table.apply_modifier(UTF8_ROUND_CORNERS);
|
|
139
|
+
table.set_content_arrangement(ContentArrangement::Dynamic);
|
|
140
|
+
|
|
141
|
+
table.set_header(vec![
|
|
142
|
+
Cell::new("IP Address").add_attribute(Attribute::Bold),
|
|
143
|
+
Cell::new("Confidence").add_attribute(Attribute::Bold),
|
|
144
|
+
Cell::new("Score").add_attribute(Attribute::Bold),
|
|
145
|
+
Cell::new("Source").add_attribute(Attribute::Bold),
|
|
146
|
+
Cell::new("Direct Server").add_attribute(Attribute::Bold),
|
|
147
|
+
Cell::new("Analysis / Reason").add_attribute(Attribute::Bold),
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
for finding in &report.findings {
|
|
151
|
+
let conf_cell = match finding.confidence {
|
|
152
|
+
ConfidenceLevel::Confirmed => Cell::new("CONFIRMED")
|
|
153
|
+
.fg(Color::Red)
|
|
154
|
+
.add_attribute(Attribute::Bold),
|
|
155
|
+
ConfidenceLevel::High => Cell::new("HIGH")
|
|
156
|
+
.fg(Color::Yellow)
|
|
157
|
+
.add_attribute(Attribute::Bold),
|
|
158
|
+
ConfidenceLevel::Medium => Cell::new("MEDIUM").fg(Color::Cyan),
|
|
159
|
+
ConfidenceLevel::Low => Cell::new("LOW").fg(Color::DarkGrey),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let score_str = format!("{}%", finding.confidence_score);
|
|
163
|
+
let score_cell = match finding.confidence {
|
|
164
|
+
ConfidenceLevel::Confirmed => Cell::new(&score_str)
|
|
165
|
+
.fg(Color::Red)
|
|
166
|
+
.add_attribute(Attribute::Bold),
|
|
167
|
+
ConfidenceLevel::High => Cell::new(&score_str).fg(Color::Yellow),
|
|
168
|
+
_ => Cell::new(&score_str),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
let server_name = finding
|
|
172
|
+
.successful_probes
|
|
173
|
+
.first()
|
|
174
|
+
.and_then(|p| p.server_header.as_deref())
|
|
175
|
+
.unwrap_or("N/A");
|
|
176
|
+
|
|
177
|
+
let source_str = finding.discovery_source.to_string();
|
|
178
|
+
|
|
179
|
+
table.add_row(vec![
|
|
180
|
+
Cell::new(finding.candidate_ip.to_string()).add_attribute(Attribute::Bold),
|
|
181
|
+
conf_cell,
|
|
182
|
+
score_cell,
|
|
183
|
+
Cell::new(source_str),
|
|
184
|
+
Cell::new(server_name),
|
|
185
|
+
Cell::new(&finding.confidence_reason),
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
out.push_str(&table.to_string());
|
|
190
|
+
out.push_str("\n\n");
|
|
191
|
+
|
|
192
|
+
// Risk Summary Stats
|
|
193
|
+
out.push_str(&format!(
|
|
194
|
+
" 📊 Origin Leak Summary: {} Confirmed, {} High, {} Medium, {} Low\n\n",
|
|
195
|
+
report
|
|
196
|
+
.summary
|
|
197
|
+
.origins_confirmed
|
|
198
|
+
.to_string()
|
|
199
|
+
.bright_red()
|
|
200
|
+
.bold(),
|
|
201
|
+
report
|
|
202
|
+
.summary
|
|
203
|
+
.high_confidence_origins
|
|
204
|
+
.to_string()
|
|
205
|
+
.bright_yellow()
|
|
206
|
+
.bold(),
|
|
207
|
+
report
|
|
208
|
+
.summary
|
|
209
|
+
.medium_confidence_origins
|
|
210
|
+
.to_string()
|
|
211
|
+
.bright_cyan(),
|
|
212
|
+
report.summary.low_confidence_origins.to_string().dimmed()
|
|
213
|
+
));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Remediation Plan Section
|
|
217
|
+
if report.summary.is_origin_leaked {
|
|
218
|
+
out.push_str(
|
|
219
|
+
&"─── ACTIONABLE REMEDIATION STEPS ──────────────────────────────────────────────\n"
|
|
220
|
+
.bright_red()
|
|
221
|
+
.bold()
|
|
222
|
+
.to_string(),
|
|
223
|
+
);
|
|
224
|
+
for step in &report.remediation {
|
|
225
|
+
out.push_str(&format!(
|
|
226
|
+
"\n [{}] {} ({})\n",
|
|
227
|
+
step.id.bold().yellow(),
|
|
228
|
+
step.title.bold().white(),
|
|
229
|
+
step.priority.bright_red()
|
|
230
|
+
));
|
|
231
|
+
out.push_str(&format!(" 📝 {}\n", step.description));
|
|
232
|
+
if !step.commands.is_empty() {
|
|
233
|
+
out.push_str(" 💻 Commands:\n");
|
|
234
|
+
for cmd in &step.commands {
|
|
235
|
+
out.push_str(&format!(" {}\n", cmd.bright_cyan()));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
out.push_str(&format!(" 🔗 Docs: {}\n", step.doc_url.dimmed()));
|
|
239
|
+
}
|
|
240
|
+
out.push('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
out
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Renders JSON format
|
|
247
|
+
pub fn render_json(report: &ScanReport) -> Result<String> {
|
|
248
|
+
serde_json::to_string_pretty(report).map_err(HunterError::from)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Renders OASIS SARIF v2.1.0 format
|
|
252
|
+
pub fn render_sarif(report: &ScanReport) -> Result<String> {
|
|
253
|
+
let mut results = Vec::new();
|
|
254
|
+
|
|
255
|
+
for finding in &report.findings {
|
|
256
|
+
let (rule_id, level, title) = match finding.confidence {
|
|
257
|
+
ConfidenceLevel::Confirmed => (
|
|
258
|
+
"CF-ORIGIN-LEAK-CONFIRMED",
|
|
259
|
+
"error",
|
|
260
|
+
"Cloudflare Origin IP Directly Exposed (Confirmed)",
|
|
261
|
+
),
|
|
262
|
+
ConfidenceLevel::High => (
|
|
263
|
+
"CF-ORIGIN-LEAK-HIGH",
|
|
264
|
+
"error",
|
|
265
|
+
"Cloudflare Origin IP Likely Exposed (High Confidence)",
|
|
266
|
+
),
|
|
267
|
+
ConfidenceLevel::Medium => (
|
|
268
|
+
"CF-ORIGIN-LEAK-MEDIUM",
|
|
269
|
+
"warning",
|
|
270
|
+
"Potential Cloudflare Origin IP Exposed (Medium Confidence)",
|
|
271
|
+
),
|
|
272
|
+
ConfidenceLevel::Low => (
|
|
273
|
+
"CF-ORIGIN-LEAK-LOW",
|
|
274
|
+
"note",
|
|
275
|
+
"Unverified Candidate Origin IP (Low Confidence)",
|
|
276
|
+
),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
let result_obj = json!({
|
|
280
|
+
"ruleId": rule_id,
|
|
281
|
+
"level": level,
|
|
282
|
+
"message": {
|
|
283
|
+
"text": format!(
|
|
284
|
+
"{}: IP {} discovered via {}. {}",
|
|
285
|
+
title, finding.candidate_ip, finding.discovery_source, finding.confidence_reason
|
|
286
|
+
)
|
|
287
|
+
},
|
|
288
|
+
"properties": {
|
|
289
|
+
"candidateIp": finding.candidate_ip.to_string(),
|
|
290
|
+
"confidenceLevel": finding.confidence.to_string(),
|
|
291
|
+
"confidenceScore": finding.confidence_score,
|
|
292
|
+
"hostname": finding.hostname,
|
|
293
|
+
"discoverySource": finding.discovery_source.to_string()
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
results.push(result_obj);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let sarif_obj = json!({
|
|
301
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
302
|
+
"version": "2.1.0",
|
|
303
|
+
"runs": [
|
|
304
|
+
{
|
|
305
|
+
"tool": {
|
|
306
|
+
"driver": {
|
|
307
|
+
"name": "cf-origin-hunter",
|
|
308
|
+
"version": "0.1.0",
|
|
309
|
+
"informationUri": "https://github.com/cloudflare-security/cf-origin-hunter",
|
|
310
|
+
"rules": [
|
|
311
|
+
{
|
|
312
|
+
"id": "CF-ORIGIN-LEAK-CONFIRMED",
|
|
313
|
+
"shortDescription": { "text": "Cloudflare Origin IP Directly Exposed (Confirmed)" },
|
|
314
|
+
"defaultConfiguration": { "level": "error" }
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"id": "CF-ORIGIN-LEAK-HIGH",
|
|
318
|
+
"shortDescription": { "text": "Cloudflare Origin IP Likely Exposed (High Confidence)" },
|
|
319
|
+
"defaultConfiguration": { "level": "error" }
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"id": "CF-ORIGIN-LEAK-MEDIUM",
|
|
323
|
+
"shortDescription": { "text": "Potential Cloudflare Origin IP Exposed (Medium Confidence)" },
|
|
324
|
+
"defaultConfiguration": { "level": "warning" }
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"id": "CF-ORIGIN-LEAK-LOW",
|
|
328
|
+
"shortDescription": { "text": "Unverified Candidate Origin IP (Low Confidence)" },
|
|
329
|
+
"defaultConfiguration": { "level": "note" }
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"results": results
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
serde_json::to_string_pretty(&sarif_obj).map_err(HunterError::from)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Renders a modern HTML report
|
|
343
|
+
pub fn render_html(report: &ScanReport) -> String {
|
|
344
|
+
let mut rows = String::new();
|
|
345
|
+
|
|
346
|
+
for finding in &report.findings {
|
|
347
|
+
let (badge_class, badge_label) = match finding.confidence {
|
|
348
|
+
ConfidenceLevel::Confirmed => ("badge-confirmed", "CONFIRMED 100%"),
|
|
349
|
+
ConfidenceLevel::High => ("badge-high", "HIGH"),
|
|
350
|
+
ConfidenceLevel::Medium => ("badge-medium", "MEDIUM"),
|
|
351
|
+
ConfidenceLevel::Low => ("badge-low", "LOW"),
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
let server_name = finding
|
|
355
|
+
.successful_probes
|
|
356
|
+
.first()
|
|
357
|
+
.and_then(|p| p.server_header.as_deref())
|
|
358
|
+
.unwrap_or("N/A");
|
|
359
|
+
|
|
360
|
+
rows.push_str(&format!(
|
|
361
|
+
r#"<tr>
|
|
362
|
+
<td><code>{}</code></td>
|
|
363
|
+
<td><span class="badge {}">{}</span></td>
|
|
364
|
+
<td><strong>{}%</strong></td>
|
|
365
|
+
<td>{}</td>
|
|
366
|
+
<td><code>{}</code></td>
|
|
367
|
+
<td>{}</td>
|
|
368
|
+
</tr>"#,
|
|
369
|
+
finding.candidate_ip,
|
|
370
|
+
badge_class,
|
|
371
|
+
badge_label,
|
|
372
|
+
finding.confidence_score,
|
|
373
|
+
finding.discovery_source,
|
|
374
|
+
server_name,
|
|
375
|
+
finding.confidence_reason
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let mut rem_cards = String::new();
|
|
380
|
+
for step in &report.remediation {
|
|
381
|
+
let mut cmds = String::new();
|
|
382
|
+
for cmd in &step.commands {
|
|
383
|
+
cmds.push_str(&format!("<code>{}</code>\n", cmd));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
rem_cards.push_str(&format!(
|
|
387
|
+
r#"<div class="rem-card">
|
|
388
|
+
<div class="rem-header">
|
|
389
|
+
<span class="rem-id">{}</span>
|
|
390
|
+
<h3>{}</h3>
|
|
391
|
+
<span class="badge-priority">{}</span>
|
|
392
|
+
</div>
|
|
393
|
+
<p>{}</p>
|
|
394
|
+
<pre>{}</pre>
|
|
395
|
+
<a href="{}" target="_blank" rel="noreferrer">Cloudflare Documentation →</a>
|
|
396
|
+
</div>"#,
|
|
397
|
+
step.id, step.title, step.priority, step.description, cmds, step.doc_url
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
format!(
|
|
402
|
+
r#"<!DOCTYPE html>
|
|
403
|
+
<html lang="en">
|
|
404
|
+
<head>
|
|
405
|
+
<meta charset="UTF-8">
|
|
406
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
407
|
+
<title>Cloudflare Origin Hunter Audit - {}</title>
|
|
408
|
+
<style>
|
|
409
|
+
:root {{
|
|
410
|
+
--bg: #0f172a;
|
|
411
|
+
--card-bg: #1e293b;
|
|
412
|
+
--text: #f8fafc;
|
|
413
|
+
--text-muted: #94a3b8;
|
|
414
|
+
--accent: #f97316;
|
|
415
|
+
--border: #334155;
|
|
416
|
+
--red: #ef4444;
|
|
417
|
+
--yellow: #f59e0b;
|
|
418
|
+
--cyan: #06b6d4;
|
|
419
|
+
--green: #10b981;
|
|
420
|
+
}}
|
|
421
|
+
body {{
|
|
422
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
423
|
+
background: var(--bg);
|
|
424
|
+
color: var(--text);
|
|
425
|
+
margin: 0;
|
|
426
|
+
padding: 2rem;
|
|
427
|
+
line-height: 1.5;
|
|
428
|
+
}}
|
|
429
|
+
.container {{
|
|
430
|
+
max-width: 1200px;
|
|
431
|
+
margin: 0 auto;
|
|
432
|
+
}}
|
|
433
|
+
header {{
|
|
434
|
+
background: var(--card-bg);
|
|
435
|
+
border: 1px solid var(--border);
|
|
436
|
+
border-radius: 12px;
|
|
437
|
+
padding: 1.5rem;
|
|
438
|
+
margin-bottom: 2rem;
|
|
439
|
+
}}
|
|
440
|
+
h1 {{
|
|
441
|
+
margin: 0 0 0.5rem 0;
|
|
442
|
+
color: var(--accent);
|
|
443
|
+
}}
|
|
444
|
+
.grid {{
|
|
445
|
+
display: grid;
|
|
446
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
447
|
+
gap: 1rem;
|
|
448
|
+
margin-top: 1rem;
|
|
449
|
+
}}
|
|
450
|
+
.stat-card {{
|
|
451
|
+
background: #0f172a;
|
|
452
|
+
padding: 1rem;
|
|
453
|
+
border-radius: 8px;
|
|
454
|
+
border: 1px solid var(--border);
|
|
455
|
+
}}
|
|
456
|
+
.stat-val {{
|
|
457
|
+
font-size: 1.8rem;
|
|
458
|
+
font-weight: bold;
|
|
459
|
+
}}
|
|
460
|
+
table {{
|
|
461
|
+
width: 100%;
|
|
462
|
+
border-collapse: collapse;
|
|
463
|
+
background: var(--card-bg);
|
|
464
|
+
border-radius: 12px;
|
|
465
|
+
overflow: hidden;
|
|
466
|
+
border: 1px solid var(--border);
|
|
467
|
+
margin-bottom: 2rem;
|
|
468
|
+
}}
|
|
469
|
+
th, td {{
|
|
470
|
+
padding: 0.8rem 1rem;
|
|
471
|
+
text-align: left;
|
|
472
|
+
border-bottom: 1px solid var(--border);
|
|
473
|
+
}}
|
|
474
|
+
th {{
|
|
475
|
+
background: #0f172a;
|
|
476
|
+
color: var(--text-muted);
|
|
477
|
+
font-weight: 600;
|
|
478
|
+
}}
|
|
479
|
+
.badge {{
|
|
480
|
+
padding: 0.25rem 0.5rem;
|
|
481
|
+
border-radius: 4px;
|
|
482
|
+
font-size: 0.75rem;
|
|
483
|
+
font-weight: bold;
|
|
484
|
+
display: inline-block;
|
|
485
|
+
}}
|
|
486
|
+
.badge-confirmed {{ background: #991b1b; color: #fee2e2; }}
|
|
487
|
+
.badge-high {{ background: #854d0e; color: #fef9c3; }}
|
|
488
|
+
.badge-medium {{ background: #155e75; color: #cffafe; }}
|
|
489
|
+
.badge-low {{ background: #374151; color: #e5e7eb; }}
|
|
490
|
+
.rem-card {{
|
|
491
|
+
background: var(--card-bg);
|
|
492
|
+
border: 1px solid var(--border);
|
|
493
|
+
border-radius: 12px;
|
|
494
|
+
padding: 1.25rem;
|
|
495
|
+
margin-bottom: 1rem;
|
|
496
|
+
}}
|
|
497
|
+
.rem-header {{
|
|
498
|
+
display: flex;
|
|
499
|
+
align-items: center;
|
|
500
|
+
gap: 0.75rem;
|
|
501
|
+
}}
|
|
502
|
+
.rem-id {{
|
|
503
|
+
background: var(--accent);
|
|
504
|
+
color: #000;
|
|
505
|
+
font-weight: bold;
|
|
506
|
+
padding: 0.2rem 0.5rem;
|
|
507
|
+
border-radius: 4px;
|
|
508
|
+
}}
|
|
509
|
+
pre {{
|
|
510
|
+
background: #0f172a;
|
|
511
|
+
padding: 1rem;
|
|
512
|
+
border-radius: 8px;
|
|
513
|
+
overflow-x: auto;
|
|
514
|
+
border: 1px solid var(--border);
|
|
515
|
+
}}
|
|
516
|
+
code {{
|
|
517
|
+
font-family: monospace;
|
|
518
|
+
color: #38bdf8;
|
|
519
|
+
}}
|
|
520
|
+
a {{
|
|
521
|
+
color: var(--accent);
|
|
522
|
+
text-decoration: none;
|
|
523
|
+
}}
|
|
524
|
+
</style>
|
|
525
|
+
</head>
|
|
526
|
+
<body>
|
|
527
|
+
<div class="container">
|
|
528
|
+
<header>
|
|
529
|
+
<h1>🛡️ Cloudflare Origin Hunter Audit</h1>
|
|
530
|
+
<p>Target: <strong>{}</strong> | Generated at: {}</p>
|
|
531
|
+
<div class="grid">
|
|
532
|
+
<div class="stat-card">
|
|
533
|
+
<div style="color: var(--text-muted);">Confirmed Origins</div>
|
|
534
|
+
<div class="stat-val" style="color: var(--red);">{}</div>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="stat-card">
|
|
537
|
+
<div style="color: var(--text-muted);">High Confidence</div>
|
|
538
|
+
<div class="stat-val" style="color: var(--yellow);">{}</div>
|
|
539
|
+
</div>
|
|
540
|
+
<div class="stat-card">
|
|
541
|
+
<div style="color: var(--text-muted);">Medium Candidates</div>
|
|
542
|
+
<div class="stat-val" style="color: var(--cyan);">{}</div>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="stat-card">
|
|
545
|
+
<div style="color: var(--text-muted);">Proxy Status</div>
|
|
546
|
+
<div class="stat-val" style="color: var(--green);">{}</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
</header>
|
|
550
|
+
|
|
551
|
+
<h2>Discovered Candidate Findings</h2>
|
|
552
|
+
<table>
|
|
553
|
+
<thead>
|
|
554
|
+
<tr>
|
|
555
|
+
<th>IP Address</th>
|
|
556
|
+
<th>Confidence</th>
|
|
557
|
+
<th>Score</th>
|
|
558
|
+
<th>Source</th>
|
|
559
|
+
<th>Direct Server</th>
|
|
560
|
+
<th>Reason</th>
|
|
561
|
+
</tr>
|
|
562
|
+
</thead>
|
|
563
|
+
<tbody>
|
|
564
|
+
{}
|
|
565
|
+
</tbody>
|
|
566
|
+
</table>
|
|
567
|
+
|
|
568
|
+
<h2>Actionable Mitigation Plan</h2>
|
|
569
|
+
{}
|
|
570
|
+
</div>
|
|
571
|
+
</body>
|
|
572
|
+
</html>"#,
|
|
573
|
+
report.summary.target_domain,
|
|
574
|
+
report.summary.target_domain,
|
|
575
|
+
report.summary.scanned_at.format("%Y-%m-%d %H:%M:%S UTC"),
|
|
576
|
+
report.summary.origins_confirmed,
|
|
577
|
+
report.summary.high_confidence_origins,
|
|
578
|
+
report.summary.medium_confidence_origins,
|
|
579
|
+
if report.summary.is_behind_cloudflare {
|
|
580
|
+
"Cloudflare Proxied"
|
|
581
|
+
} else {
|
|
582
|
+
"Direct"
|
|
583
|
+
},
|
|
584
|
+
rows,
|
|
585
|
+
rem_cards
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#[cfg(test)]
|
|
590
|
+
mod tests {
|
|
591
|
+
use super::*;
|
|
592
|
+
use crate::origin::mock::run_mock_scan;
|
|
593
|
+
|
|
594
|
+
#[test]
|
|
595
|
+
fn test_render_text() {
|
|
596
|
+
let report = run_mock_scan("example.com");
|
|
597
|
+
let txt = render_text(&report);
|
|
598
|
+
assert!(txt.contains("CLOUDFLARE ORIGIN HUNTER"));
|
|
599
|
+
assert!(txt.contains("198.51.100.42"));
|
|
600
|
+
assert!(txt.contains("CONFIRMED"));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#[test]
|
|
604
|
+
fn test_render_json() {
|
|
605
|
+
let report = run_mock_scan("example.com");
|
|
606
|
+
let json_str = render_json(&report).unwrap();
|
|
607
|
+
assert!(json_str.contains("\"target_domain\": \"example.com\""));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#[test]
|
|
611
|
+
fn test_render_sarif() {
|
|
612
|
+
let report = run_mock_scan("example.com");
|
|
613
|
+
let sarif_str = render_sarif(&report).unwrap();
|
|
614
|
+
assert!(sarif_str.contains("CF-ORIGIN-LEAK-CONFIRMED"));
|
|
615
|
+
assert!(sarif_str.contains("sarif-schema-2.1.0.json"));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
#[test]
|
|
619
|
+
fn test_render_html() {
|
|
620
|
+
let report = run_mock_scan("example.com");
|
|
621
|
+
let html_str = render_html(&report);
|
|
622
|
+
assert!(html_str.contains("<html"));
|
|
623
|
+
assert!(html_str.contains("198.51.100.42"));
|
|
624
|
+
}
|
|
625
|
+
}
|