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,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
+ }