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,217 @@
|
|
|
1
|
+
use crate::origin::cloudflare::partition_ips;
|
|
2
|
+
use crate::origin::confidence::calculate_confidence;
|
|
3
|
+
use crate::origin::dns::{create_resolver, resolve_ips};
|
|
4
|
+
use crate::origin::enumerator::{EnumeratorOptions, enumerate_candidate_ips, load_wordlist_file};
|
|
5
|
+
use crate::origin::error::{HunterError, Result};
|
|
6
|
+
use crate::origin::models::{
|
|
7
|
+
CandidateIp, ConfidenceLevel, HunterFinding, ProbeResult, ScanReport, ScanSummary,
|
|
8
|
+
};
|
|
9
|
+
use crate::origin::prober::{create_probe_client, fetch_target_baseline, probe_candidate_ip};
|
|
10
|
+
use crate::origin::remediation::generate_remediation_plan;
|
|
11
|
+
use chrono::Utc;
|
|
12
|
+
use futures::stream::{self, StreamExt};
|
|
13
|
+
use std::net::IpAddr;
|
|
14
|
+
use std::path::PathBuf;
|
|
15
|
+
use std::time::Instant;
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Clone)]
|
|
18
|
+
pub struct ScanOptions {
|
|
19
|
+
pub concurrency: usize,
|
|
20
|
+
pub timeout_secs: u64,
|
|
21
|
+
pub probe_ports: Vec<u16>,
|
|
22
|
+
pub enable_crtsh: bool,
|
|
23
|
+
pub enable_subdomains: bool,
|
|
24
|
+
pub enable_dns: bool,
|
|
25
|
+
pub wordlist_path: Option<PathBuf>,
|
|
26
|
+
pub min_confidence: ConfidenceLevel,
|
|
27
|
+
pub verbose: bool,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Default for ScanOptions {
|
|
31
|
+
fn default() -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
concurrency: 10,
|
|
34
|
+
timeout_secs: 5,
|
|
35
|
+
probe_ports: vec![80, 443, 8080, 8443],
|
|
36
|
+
enable_crtsh: true,
|
|
37
|
+
enable_subdomains: true,
|
|
38
|
+
enable_dns: true,
|
|
39
|
+
wordlist_path: None,
|
|
40
|
+
min_confidence: ConfidenceLevel::Low,
|
|
41
|
+
verbose: false,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Executes a full live audit against the specified target domain
|
|
47
|
+
pub async fn run_scan(target_domain: &str, options: &ScanOptions) -> Result<ScanReport> {
|
|
48
|
+
let start_time = Instant::now();
|
|
49
|
+
let scanned_at = Utc::now();
|
|
50
|
+
let root = target_domain.trim().to_lowercase();
|
|
51
|
+
|
|
52
|
+
if root.is_empty() {
|
|
53
|
+
return Err(HunterError::InvalidTarget(
|
|
54
|
+
"Target domain cannot be empty".into(),
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let resolver = create_resolver()?;
|
|
59
|
+
let http_client = create_probe_client(options.timeout_secs)?;
|
|
60
|
+
|
|
61
|
+
// 1. Establish Domain Baseline
|
|
62
|
+
let resolved_ips = resolve_ips(&resolver, &root).await.unwrap_or_default();
|
|
63
|
+
let (cf_edge_ips, direct_ips) = partition_ips(&resolved_ips);
|
|
64
|
+
let is_behind_cf = !cf_edge_ips.is_empty();
|
|
65
|
+
|
|
66
|
+
let baseline = fetch_target_baseline(
|
|
67
|
+
&http_client,
|
|
68
|
+
&root,
|
|
69
|
+
&resolved_ips,
|
|
70
|
+
is_behind_cf,
|
|
71
|
+
&cf_edge_ips,
|
|
72
|
+
&direct_ips,
|
|
73
|
+
)
|
|
74
|
+
.await;
|
|
75
|
+
|
|
76
|
+
// 2. Candidate Origin IP Discovery
|
|
77
|
+
let custom_wordlist = if let Some(ref path) = options.wordlist_path {
|
|
78
|
+
Some(load_wordlist_file(path)?)
|
|
79
|
+
} else {
|
|
80
|
+
None
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let enum_opts = EnumeratorOptions {
|
|
84
|
+
concurrency: options.concurrency,
|
|
85
|
+
enable_crtsh: options.enable_crtsh,
|
|
86
|
+
enable_subdomains: options.enable_subdomains,
|
|
87
|
+
enable_dns: options.enable_dns,
|
|
88
|
+
custom_wordlist,
|
|
89
|
+
timeout_secs: options.timeout_secs,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let candidates = enumerate_candidate_ips(&root, &resolver, &http_client, &enum_opts).await?;
|
|
93
|
+
|
|
94
|
+
// 3. Direct HTTP/HTTPS Origin Probing
|
|
95
|
+
let probe_tasks = {
|
|
96
|
+
let mut tasks = Vec::new();
|
|
97
|
+
for candidate in &candidates {
|
|
98
|
+
for &port in &options.probe_ports {
|
|
99
|
+
tasks.push((candidate.clone(), port));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
tasks
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let mut probe_results_map: std::collections::HashMap<IpAddr, (CandidateIp, Vec<ProbeResult>)> =
|
|
106
|
+
std::collections::HashMap::new();
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
let concurrency = options.concurrency.max(1);
|
|
110
|
+
let target_for_probe = root.clone();
|
|
111
|
+
let baseline_for_probe = baseline.clone();
|
|
112
|
+
let client_for_probe = http_client.clone();
|
|
113
|
+
|
|
114
|
+
let probe_stream = stream::iter(probe_tasks).map(move |(cand, port)| {
|
|
115
|
+
let client = client_for_probe.clone();
|
|
116
|
+
let target = target_for_probe.clone();
|
|
117
|
+
let base = baseline_for_probe.clone();
|
|
118
|
+
async move {
|
|
119
|
+
let res = probe_candidate_ip(&client, cand.ip, port, &target, &base).await;
|
|
120
|
+
(cand, res)
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let mut buffered_probes = probe_stream.buffer_unordered(concurrency);
|
|
125
|
+
while let Some((cand, probe_res)) = buffered_probes.next().await {
|
|
126
|
+
probe_results_map
|
|
127
|
+
.entry(cand.ip)
|
|
128
|
+
.or_insert_with(|| (cand.clone(), Vec::new()))
|
|
129
|
+
.1
|
|
130
|
+
.push(probe_res);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 4. Verification & Confidence Scoring
|
|
135
|
+
let mut findings = Vec::new();
|
|
136
|
+
for (ip, (cand, probes)) in probe_results_map {
|
|
137
|
+
let mut successful = Vec::new();
|
|
138
|
+
let mut failed = Vec::new();
|
|
139
|
+
|
|
140
|
+
for probe in probes {
|
|
141
|
+
if probe.success {
|
|
142
|
+
successful.push(probe);
|
|
143
|
+
} else {
|
|
144
|
+
failed.push(probe);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let (confidence_lvl, score, reason) =
|
|
149
|
+
calculate_confidence(&baseline, &cand.source, &successful, &failed);
|
|
150
|
+
|
|
151
|
+
if confidence_lvl >= options.min_confidence {
|
|
152
|
+
let finding = HunterFinding {
|
|
153
|
+
candidate_ip: ip,
|
|
154
|
+
hostname: cand.hostname,
|
|
155
|
+
discovery_source: cand.source,
|
|
156
|
+
confidence: confidence_lvl,
|
|
157
|
+
confidence_score: score,
|
|
158
|
+
confidence_reason: reason,
|
|
159
|
+
successful_probes: successful,
|
|
160
|
+
failed_probes: failed,
|
|
161
|
+
is_origin_confirmed: confidence_lvl == ConfidenceLevel::Confirmed,
|
|
162
|
+
};
|
|
163
|
+
findings.push(finding);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
findings.sort_by(|a, b| {
|
|
168
|
+
b.confidence_score
|
|
169
|
+
.cmp(&a.confidence_score)
|
|
170
|
+
.then_with(|| a.candidate_ip.cmp(&b.candidate_ip))
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let duration_seconds = start_time.elapsed().as_secs_f64();
|
|
174
|
+
|
|
175
|
+
let origins_confirmed = findings
|
|
176
|
+
.iter()
|
|
177
|
+
.filter(|f| f.confidence == ConfidenceLevel::Confirmed)
|
|
178
|
+
.count();
|
|
179
|
+
let high_confidence = findings
|
|
180
|
+
.iter()
|
|
181
|
+
.filter(|f| f.confidence == ConfidenceLevel::High)
|
|
182
|
+
.count();
|
|
183
|
+
let medium_confidence = findings
|
|
184
|
+
.iter()
|
|
185
|
+
.filter(|f| f.confidence == ConfidenceLevel::Medium)
|
|
186
|
+
.count();
|
|
187
|
+
let low_confidence = findings
|
|
188
|
+
.iter()
|
|
189
|
+
.filter(|f| f.confidence == ConfidenceLevel::Low)
|
|
190
|
+
.count();
|
|
191
|
+
|
|
192
|
+
let is_origin_leaked = origins_confirmed > 0 || high_confidence > 0 || medium_confidence > 0;
|
|
193
|
+
|
|
194
|
+
let summary = ScanSummary {
|
|
195
|
+
target_domain: root,
|
|
196
|
+
scanned_at,
|
|
197
|
+
duration_seconds,
|
|
198
|
+
is_behind_cloudflare: is_behind_cf,
|
|
199
|
+
cloudflare_edge_ips: cf_edge_ips,
|
|
200
|
+
candidates_discovered: candidates.len(),
|
|
201
|
+
origins_confirmed,
|
|
202
|
+
high_confidence_origins: high_confidence,
|
|
203
|
+
medium_confidence_origins: medium_confidence,
|
|
204
|
+
low_confidence_origins: low_confidence,
|
|
205
|
+
is_origin_leaked,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let remediation = generate_remediation_plan();
|
|
209
|
+
|
|
210
|
+
Ok(ScanReport {
|
|
211
|
+
summary,
|
|
212
|
+
baseline,
|
|
213
|
+
findings,
|
|
214
|
+
candidates,
|
|
215
|
+
remediation,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
use crate::secrets::report::OutputFormat;
|
|
2
|
+
use crate::secrets::rules::types::Severity;
|
|
3
|
+
use clap::{ArgAction, Parser};
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
/// Fast, multi-threaded static asset scanner to detect Cloudflare server secret leaks in client bundles.
|
|
7
|
+
#[derive(Parser, Debug)]
|
|
8
|
+
#[command(
|
|
9
|
+
name = "cf-secret-leak-guard",
|
|
10
|
+
version,
|
|
11
|
+
about = "Prevent Cloudflare server-side secrets from leaking into client-side static assets",
|
|
12
|
+
long_about = "cf-secret-leak-guard scans static web asset directories (dist/client, dist/_astro, public) \
|
|
13
|
+
for Cloudflare server-side credentials, API tokens, Turnstile secret keys, Origin CA keys, and .dev.vars / .env values."
|
|
14
|
+
)]
|
|
15
|
+
pub struct Cli {
|
|
16
|
+
/// Target asset directories or files to scan (default: auto-detect dist/client, dist/_astro, public)
|
|
17
|
+
#[arg(value_name = "TARGETS")]
|
|
18
|
+
pub targets: Vec<PathBuf>,
|
|
19
|
+
|
|
20
|
+
/// Additional target directory or file to scan (can be specified multiple times)
|
|
21
|
+
#[arg(short = 't', long = "target", action = ArgAction::Append)]
|
|
22
|
+
pub additional_targets: Vec<PathBuf>,
|
|
23
|
+
|
|
24
|
+
/// CI gate check: Exit with code 1 if any server secrets are detected
|
|
25
|
+
#[arg(long, help = "Exit with non-zero exit code (1) if leaks are detected")]
|
|
26
|
+
pub check: bool,
|
|
27
|
+
|
|
28
|
+
/// Report output format (text, json, sarif)
|
|
29
|
+
#[arg(short = 'f', long = "format", value_enum, default_value_t = OutputFormat::Text)]
|
|
30
|
+
pub format: OutputFormat,
|
|
31
|
+
|
|
32
|
+
/// Write report output to a specified file instead of stdout
|
|
33
|
+
#[arg(short = 'o', long = "output", value_name = "FILE")]
|
|
34
|
+
pub output: Option<PathBuf>,
|
|
35
|
+
|
|
36
|
+
/// Environment or vars file (.dev.vars, .env, wrangler.jsonc) to extract secret values from
|
|
37
|
+
#[arg(short = 'e', long = "env-file", action = ArgAction::Append, value_name = "ENV_FILE")]
|
|
38
|
+
pub env_files: Vec<PathBuf>,
|
|
39
|
+
|
|
40
|
+
/// Disable auto-discovery of .env, .dev.vars, and wrangler config files
|
|
41
|
+
#[arg(long, help = "Disable automatic discovery of .dev.vars and .env files")]
|
|
42
|
+
pub no_auto_env: bool,
|
|
43
|
+
|
|
44
|
+
/// Glob patterns of files to exclude from scanning (e.g. '*.map')
|
|
45
|
+
#[arg(long = "exclude", action = ArgAction::Append, value_name = "GLOB")]
|
|
46
|
+
pub excludes: Vec<String>,
|
|
47
|
+
|
|
48
|
+
/// Specific rule IDs to ignore (e.g. 'CF-006')
|
|
49
|
+
#[arg(long = "ignore-rule", action = ArgAction::Append, value_name = "RULE_ID")]
|
|
50
|
+
pub ignore_rules: Vec<String>,
|
|
51
|
+
|
|
52
|
+
/// Specific secret strings to allowlist/ignore
|
|
53
|
+
#[arg(long = "ignore-secret", action = ArgAction::Append, value_name = "SECRET")]
|
|
54
|
+
pub ignore_secrets: Vec<String>,
|
|
55
|
+
|
|
56
|
+
/// Path to .cfsecretignore configuration file
|
|
57
|
+
#[arg(long = "ignore-file", value_name = "FILE")]
|
|
58
|
+
pub ignore_file: Option<PathBuf>,
|
|
59
|
+
|
|
60
|
+
/// Maximum file size to scan in megabytes (default: 50MB)
|
|
61
|
+
#[arg(long = "max-file-size", default_value_t = 50, value_name = "MB")]
|
|
62
|
+
pub max_file_size_mb: u64,
|
|
63
|
+
|
|
64
|
+
/// Minimum severity level to report (low, medium, high, critical)
|
|
65
|
+
#[arg(long = "min-severity", default_value_t = Severity::Low, value_name = "SEVERITY")]
|
|
66
|
+
pub min_severity: Severity,
|
|
67
|
+
|
|
68
|
+
/// Number of worker threads for parallel scanning (default: auto)
|
|
69
|
+
#[arg(long = "threads", value_name = "NUM")]
|
|
70
|
+
pub threads: Option<usize>,
|
|
71
|
+
|
|
72
|
+
/// List all built-in secret detection rules and exit
|
|
73
|
+
#[arg(
|
|
74
|
+
long = "list-rules",
|
|
75
|
+
help = "List all built-in Cloudflare secret detection rules and exit"
|
|
76
|
+
)]
|
|
77
|
+
pub list_rules: bool,
|
|
78
|
+
|
|
79
|
+
/// Suppress informative terminal messages
|
|
80
|
+
#[arg(
|
|
81
|
+
short = 'q',
|
|
82
|
+
long = "quiet",
|
|
83
|
+
help = "Quiet mode (suppress non-error output)"
|
|
84
|
+
)]
|
|
85
|
+
pub quiet: bool,
|
|
86
|
+
|
|
87
|
+
/// Show verbose scanning information and remediation steps
|
|
88
|
+
#[arg(
|
|
89
|
+
short = 'v',
|
|
90
|
+
long = "verbose",
|
|
91
|
+
help = "Verbose mode (print detailed progress and remediation)"
|
|
92
|
+
)]
|
|
93
|
+
pub verbose: bool,
|
|
94
|
+
}
|