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/cli.rs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
use clap::{Args, Parser, Subcommand, ValueEnum};
|
|
2
|
+
use clap_complete::Shell;
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
/// 🛡️ Flareguard — Unified Cloudflare Security & Architecture Guardian
|
|
6
|
+
///
|
|
7
|
+
/// High-performance Rust CLI & library providing AST secret scanning,
|
|
8
|
+
/// Worker binding verification, Zone security posture auditing, and origin IP leak hunting.
|
|
9
|
+
#[derive(Parser, Debug)]
|
|
10
|
+
#[command(
|
|
11
|
+
name = "flareguard",
|
|
12
|
+
author = "Brandon Hubbard <bhubbard@users.noreply.github.com>",
|
|
13
|
+
version = env!("CARGO_PKG_VERSION"),
|
|
14
|
+
about = "🛡️ Flareguard — Unified Cloudflare Security & Architecture Guardian",
|
|
15
|
+
long_about = "Flareguard brings all Cloudflare security checks into one high-performance tool:\n\
|
|
16
|
+
- secrets: Scan code and static bundles for exposed Cloudflare tokens and credentials\n\
|
|
17
|
+
- bindings: Validate wrangler.toml/jsonc bindings against JavaScript/TypeScript AST usage\n\
|
|
18
|
+
- zone: Audit live Cloudflare Zone security posture (SSL, HSTS, WAF, Bot Fight, DNSSEC)\n\
|
|
19
|
+
- origin: Hunt for unmasked backend origin IPs bypassing Cloudflare proxies\n\
|
|
20
|
+
- check: Run end-to-end local repository verification (secrets + bindings)\n\
|
|
21
|
+
- completions: Generate shell autocompletions (bash, zsh, fish, powershell)"
|
|
22
|
+
)]
|
|
23
|
+
pub struct Cli {
|
|
24
|
+
#[command(subcommand)]
|
|
25
|
+
pub command: Commands,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[derive(Subcommand, Debug)]
|
|
29
|
+
pub enum Commands {
|
|
30
|
+
/// 🔑 Scan files and bundles for leaked Cloudflare secrets, API tokens, and credentials
|
|
31
|
+
Secrets(crate::secrets::cli::Cli),
|
|
32
|
+
|
|
33
|
+
/// ⚡ Validate Cloudflare Worker/Pages bindings against Wrangler configuration and JS/TS ASTs
|
|
34
|
+
Bindings(crate::bindings::cli::CliArgs),
|
|
35
|
+
|
|
36
|
+
/// 🌐 Audit Cloudflare Zone security posture, WAF rules, and compliance gates
|
|
37
|
+
Zone(crate::zone::cli::AuditArgs),
|
|
38
|
+
|
|
39
|
+
/// 🎯 Hunt for unmasked backend origin IPs behind Cloudflare proxies
|
|
40
|
+
Origin(crate::origin::cli::Cli),
|
|
41
|
+
|
|
42
|
+
/// 🚀 Run comprehensive workspace verification (secrets scan + binding validation)
|
|
43
|
+
Check(CheckArgs),
|
|
44
|
+
|
|
45
|
+
/// 🐚 Generate shell autocompletion scripts
|
|
46
|
+
Completions {
|
|
47
|
+
/// The target shell
|
|
48
|
+
#[arg(value_enum)]
|
|
49
|
+
shell: Shell,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
54
|
+
pub enum CheckOutputFormat {
|
|
55
|
+
Text,
|
|
56
|
+
Json,
|
|
57
|
+
Sarif,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[derive(Args, Debug, Clone)]
|
|
61
|
+
pub struct CheckArgs {
|
|
62
|
+
/// Target directory to check (defaults to current directory)
|
|
63
|
+
#[arg(default_value = ".")]
|
|
64
|
+
pub path: PathBuf,
|
|
65
|
+
|
|
66
|
+
/// Fail with exit code 1 if any issues are detected
|
|
67
|
+
#[arg(long, default_value_t = true)]
|
|
68
|
+
pub strict: bool,
|
|
69
|
+
|
|
70
|
+
/// Output report format (text, json, sarif)
|
|
71
|
+
#[arg(short = 'f', long = "format", value_enum, default_value_t = CheckOutputFormat::Text)]
|
|
72
|
+
pub format: CheckOutputFormat,
|
|
73
|
+
|
|
74
|
+
/// Save output report to specified file path
|
|
75
|
+
#[arg(short = 'o', long = "output")]
|
|
76
|
+
pub output: Option<PathBuf>,
|
|
77
|
+
}
|
package/src/lib.rs
ADDED
package/src/main.rs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use clap::{CommandFactory, Parser};
|
|
3
|
+
use clap_complete::generate;
|
|
4
|
+
use colored::Colorize;
|
|
5
|
+
use std::collections::HashSet;
|
|
6
|
+
use std::fs;
|
|
7
|
+
use std::io;
|
|
8
|
+
use std::path::PathBuf;
|
|
9
|
+
use std::process::exit;
|
|
10
|
+
|
|
11
|
+
use flareguard::cli::{CheckArgs, CheckOutputFormat, Cli, Commands};
|
|
12
|
+
|
|
13
|
+
#[tokio::main]
|
|
14
|
+
async fn main() -> Result<()> {
|
|
15
|
+
let cli = Cli::parse();
|
|
16
|
+
|
|
17
|
+
match cli.command {
|
|
18
|
+
Commands::Secrets(args) => {
|
|
19
|
+
run_secrets_command(args).await?;
|
|
20
|
+
}
|
|
21
|
+
Commands::Bindings(args) => {
|
|
22
|
+
run_bindings_command(args)?;
|
|
23
|
+
}
|
|
24
|
+
Commands::Zone(args) => {
|
|
25
|
+
run_zone_command(args).await?;
|
|
26
|
+
}
|
|
27
|
+
Commands::Origin(args) => {
|
|
28
|
+
run_origin_command(args).await?;
|
|
29
|
+
}
|
|
30
|
+
Commands::Check(args) => {
|
|
31
|
+
run_check_command(args).await?;
|
|
32
|
+
}
|
|
33
|
+
Commands::Completions { shell } => {
|
|
34
|
+
let mut cmd = Cli::command();
|
|
35
|
+
let bin_name = cmd.get_name().to_string();
|
|
36
|
+
generate(shell, &mut cmd, bin_name, &mut io::stdout());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Ok(())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async fn run_secrets_command(cli: flareguard::secrets::cli::Cli) -> Result<()> {
|
|
44
|
+
if cli.list_rules {
|
|
45
|
+
let rules = flareguard::secrets::rules::builtin::get_builtin_rules();
|
|
46
|
+
println!("\n{}", "flareguard Built-in Secret Detection Rules:".bold());
|
|
47
|
+
println!(
|
|
48
|
+
"================================================================================"
|
|
49
|
+
);
|
|
50
|
+
for rule in rules {
|
|
51
|
+
println!(
|
|
52
|
+
"• [{}] {} (Severity: {})",
|
|
53
|
+
rule.id.bright_yellow().bold(),
|
|
54
|
+
rule.name.bold(),
|
|
55
|
+
rule.severity
|
|
56
|
+
);
|
|
57
|
+
println!(" Description: {}", rule.description);
|
|
58
|
+
println!(" Remediation: {}\n", rule.recommendation.dimmed());
|
|
59
|
+
}
|
|
60
|
+
return Ok(());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let mut ignore_filter = flareguard::secrets::ignore::IgnoreFilter::new();
|
|
64
|
+
let ignore_file_path = cli
|
|
65
|
+
.ignore_file
|
|
66
|
+
.clone()
|
|
67
|
+
.unwrap_or_else(|| PathBuf::from(".cfsecretignore"));
|
|
68
|
+
if ignore_file_path.exists() {
|
|
69
|
+
let _ = ignore_filter.load_from_file(&ignore_file_path);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for pat in &cli.excludes {
|
|
73
|
+
let _ = ignore_filter.add_exclude_pattern(pat);
|
|
74
|
+
}
|
|
75
|
+
for rule_id in &cli.ignore_rules {
|
|
76
|
+
ignore_filter.ignore_rule(rule_id);
|
|
77
|
+
}
|
|
78
|
+
for secret in &cli.ignore_secrets {
|
|
79
|
+
ignore_filter.ignore_secret(secret);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let mut target_paths = cli.targets.clone();
|
|
83
|
+
target_paths.extend(cli.additional_targets.clone());
|
|
84
|
+
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
85
|
+
if target_paths.is_empty() {
|
|
86
|
+
target_paths = flareguard::secrets::scanner::discover_default_targets(¤t_dir);
|
|
87
|
+
if target_paths.is_empty() {
|
|
88
|
+
target_paths.push(current_dir.clone());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let mut rules = flareguard::secrets::rules::builtin::get_builtin_rules();
|
|
93
|
+
if !cli.no_auto_env {
|
|
94
|
+
let env_files = flareguard::secrets::env_parser::discover_env_files(&[current_dir]);
|
|
95
|
+
for f in env_files {
|
|
96
|
+
if let Ok(parsed) = flareguard::secrets::env_parser::parse_env_file(&f) {
|
|
97
|
+
rules.extend(flareguard::secrets::env_parser::env_secrets_to_rules(
|
|
98
|
+
&parsed,
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for env_path in &cli.env_files {
|
|
105
|
+
if let Ok(parsed) = flareguard::secrets::env_parser::parse_env_file(env_path) {
|
|
106
|
+
rules.extend(flareguard::secrets::env_parser::env_secrets_to_rules(
|
|
107
|
+
&parsed,
|
|
108
|
+
));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let options = flareguard::secrets::scanner::ScannerOptions {
|
|
113
|
+
max_file_size_bytes: cli.max_file_size_mb * 1024 * 1024,
|
|
114
|
+
min_severity: cli.min_severity,
|
|
115
|
+
follow_symlinks: false,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let result =
|
|
119
|
+
flareguard::secrets::scanner::scan_targets(&target_paths, &rules, &ignore_filter, &options);
|
|
120
|
+
|
|
121
|
+
let rendered = flareguard::secrets::report::render_report(
|
|
122
|
+
cli.format,
|
|
123
|
+
&result,
|
|
124
|
+
&rules,
|
|
125
|
+
env!("CARGO_PKG_VERSION"),
|
|
126
|
+
cli.verbose,
|
|
127
|
+
)
|
|
128
|
+
.map_err(|e| anyhow::anyhow!("Error rendering report: {}", e))?;
|
|
129
|
+
|
|
130
|
+
if let Some(ref out_path) = cli.output {
|
|
131
|
+
fs::write(out_path, &rendered)?;
|
|
132
|
+
if !cli.quiet {
|
|
133
|
+
println!(
|
|
134
|
+
"Report successfully written to {}",
|
|
135
|
+
out_path.display().to_string().cyan()
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} else if !cli.quiet
|
|
139
|
+
|| !result.findings.is_empty()
|
|
140
|
+
|| cli.format != flareguard::secrets::report::OutputFormat::Text
|
|
141
|
+
{
|
|
142
|
+
println!("{}", rendered);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if cli.check && !result.findings.is_empty() {
|
|
146
|
+
exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Ok(())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn run_bindings_command(args: flareguard::bindings::cli::CliArgs) -> Result<()> {
|
|
153
|
+
let config_path = if let Some(path) = args.config.clone() {
|
|
154
|
+
if !path.exists() {
|
|
155
|
+
eprintln!(
|
|
156
|
+
"Error: Wrangler config file not found at: {}",
|
|
157
|
+
path.display()
|
|
158
|
+
);
|
|
159
|
+
exit(1);
|
|
160
|
+
}
|
|
161
|
+
Some(path)
|
|
162
|
+
} else {
|
|
163
|
+
let search_dir = args
|
|
164
|
+
.paths
|
|
165
|
+
.first()
|
|
166
|
+
.cloned()
|
|
167
|
+
.unwrap_or_else(|| PathBuf::from("."));
|
|
168
|
+
flareguard::bindings::wrangler::find_wrangler_config(&search_dir)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
let wrangler_config = match config_path {
|
|
172
|
+
Some(ref path) => match flareguard::bindings::wrangler::parse_wrangler_config(path) {
|
|
173
|
+
Ok(cfg) => Some(cfg),
|
|
174
|
+
Err(e) => {
|
|
175
|
+
eprintln!(
|
|
176
|
+
"Error parsing wrangler configuration ({}): {}",
|
|
177
|
+
path.display(),
|
|
178
|
+
e
|
|
179
|
+
);
|
|
180
|
+
exit(1);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
None => None,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let options = flareguard::bindings::validator::ValidatorOptions {
|
|
187
|
+
target_paths: args.paths.clone(),
|
|
188
|
+
environment: args.environment.clone(),
|
|
189
|
+
ignore_unused: args.ignore_unused.into_iter().collect::<HashSet<_>>(),
|
|
190
|
+
ignore_undeclared: args.ignore_undeclared.into_iter().collect::<HashSet<_>>(),
|
|
191
|
+
strict: args.strict,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let report =
|
|
195
|
+
match flareguard::bindings::validator::validate_project(wrangler_config.as_ref(), &options)
|
|
196
|
+
{
|
|
197
|
+
Ok(r) => r,
|
|
198
|
+
Err(e) => {
|
|
199
|
+
eprintln!("Validation error: {}", e);
|
|
200
|
+
exit(1);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let format = match args.format {
|
|
205
|
+
flareguard::bindings::cli::CliFormat::Text => {
|
|
206
|
+
flareguard::bindings::types::OutputFormat::Text
|
|
207
|
+
}
|
|
208
|
+
flareguard::bindings::cli::CliFormat::Json => {
|
|
209
|
+
flareguard::bindings::types::OutputFormat::Json
|
|
210
|
+
}
|
|
211
|
+
flareguard::bindings::cli::CliFormat::Sarif => {
|
|
212
|
+
flareguard::bindings::types::OutputFormat::Sarif
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if let Err(e) = flareguard::bindings::reporter::render_report(&report, format) {
|
|
217
|
+
eprintln!("Error writing report: {}", e);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if args.check || args.strict {
|
|
221
|
+
if !report.undeclared_accesses.is_empty() {
|
|
222
|
+
exit(1);
|
|
223
|
+
}
|
|
224
|
+
if args.strict && !report.ghost_bindings.is_empty() {
|
|
225
|
+
exit(2);
|
|
226
|
+
}
|
|
227
|
+
} else if !report.undeclared_accesses.is_empty() {
|
|
228
|
+
exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Ok(())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async fn run_zone_command(args: flareguard::zone::cli::AuditArgs) -> Result<()> {
|
|
235
|
+
let report = flareguard::zone::run_audit(&args).await?;
|
|
236
|
+
let rendered = flareguard::zone::output_report(&report, &args)?;
|
|
237
|
+
|
|
238
|
+
if args.output.is_none() && !args.quiet {
|
|
239
|
+
println!("{}", rendered);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let (passed, failure_reasons) = flareguard::zone::evaluate_compliance_gates(&report, &args);
|
|
243
|
+
if !passed {
|
|
244
|
+
if !args.quiet {
|
|
245
|
+
for reason in failure_reasons {
|
|
246
|
+
eprintln!("{} {}", "✗".red().bold(), reason);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Ok(())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async fn run_origin_command(cli: flareguard::origin::cli::Cli) -> Result<()> {
|
|
256
|
+
let output_format = cli.get_output_format();
|
|
257
|
+
let min_confidence = cli.get_min_confidence();
|
|
258
|
+
|
|
259
|
+
let report_res = if cli.mock {
|
|
260
|
+
let target = cli.target.as_deref().unwrap_or("example-corp.com");
|
|
261
|
+
Ok(flareguard::origin::mock::run_mock_scan(target))
|
|
262
|
+
} else {
|
|
263
|
+
match cli.target.as_deref() {
|
|
264
|
+
Some(domain) => {
|
|
265
|
+
let options = flareguard::origin::scanner::ScanOptions {
|
|
266
|
+
concurrency: cli.concurrency,
|
|
267
|
+
timeout_secs: cli.timeout,
|
|
268
|
+
probe_ports: cli.get_probe_ports(),
|
|
269
|
+
enable_crtsh: !cli.no_crtsh,
|
|
270
|
+
enable_subdomains: !cli.no_subdomains,
|
|
271
|
+
enable_dns: !cli.no_dns,
|
|
272
|
+
wordlist_path: cli.wordlist.clone(),
|
|
273
|
+
min_confidence,
|
|
274
|
+
verbose: cli.verbose,
|
|
275
|
+
};
|
|
276
|
+
flareguard::origin::scanner::run_scan(domain, &options).await
|
|
277
|
+
}
|
|
278
|
+
None => {
|
|
279
|
+
eprintln!(
|
|
280
|
+
"{} Please specify a target domain (e.g. `flareguard origin example.com`) or use `--mock`.",
|
|
281
|
+
"Error:".bright_red().bold()
|
|
282
|
+
);
|
|
283
|
+
exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let report = match report_res {
|
|
289
|
+
Ok(r) => r,
|
|
290
|
+
Err(e) => {
|
|
291
|
+
eprintln!(
|
|
292
|
+
"{} Failed to complete scan: {}",
|
|
293
|
+
"Error:".bright_red().bold(),
|
|
294
|
+
e
|
|
295
|
+
);
|
|
296
|
+
exit(1);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
let formatted_output = match flareguard::origin::report::render_report(&report, output_format) {
|
|
301
|
+
Ok(out) => out,
|
|
302
|
+
Err(e) => {
|
|
303
|
+
eprintln!(
|
|
304
|
+
"{} Failed to format report: {}",
|
|
305
|
+
"Error:".bright_red().bold(),
|
|
306
|
+
e
|
|
307
|
+
);
|
|
308
|
+
exit(1);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if let Some(ref path) = cli.output {
|
|
313
|
+
if let Err(e) = fs::write(path, &formatted_output) {
|
|
314
|
+
eprintln!(
|
|
315
|
+
"{} Failed to write report to {}: {}",
|
|
316
|
+
"Error:".bright_red().bold(),
|
|
317
|
+
path.display(),
|
|
318
|
+
e
|
|
319
|
+
);
|
|
320
|
+
exit(1);
|
|
321
|
+
}
|
|
322
|
+
println!(
|
|
323
|
+
"{} Report successfully saved to {}",
|
|
324
|
+
"Success:".bright_green().bold(),
|
|
325
|
+
path.display().to_string().cyan()
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
println!("{}", formatted_output);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if cli.check {
|
|
332
|
+
let check_threshold = if cli.min_confidence == "LOW" {
|
|
333
|
+
flareguard::origin::models::ConfidenceLevel::High
|
|
334
|
+
} else {
|
|
335
|
+
min_confidence
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
let failing_findings: Vec<_> = report
|
|
339
|
+
.findings
|
|
340
|
+
.iter()
|
|
341
|
+
.filter(|f| f.confidence >= check_threshold)
|
|
342
|
+
.collect();
|
|
343
|
+
|
|
344
|
+
if !failing_findings.is_empty() {
|
|
345
|
+
eprintln!(
|
|
346
|
+
"\n{} CI Check Failed: {} unmasked origin IP(s) detected with confidence >= {}!",
|
|
347
|
+
"FAIL:".bright_red().bold(),
|
|
348
|
+
failing_findings.len(),
|
|
349
|
+
check_threshold
|
|
350
|
+
);
|
|
351
|
+
exit(1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
Ok(())
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async fn run_check_command(args: CheckArgs) -> Result<()> {
|
|
359
|
+
if args.format == CheckOutputFormat::Text {
|
|
360
|
+
println!(
|
|
361
|
+
"{}",
|
|
362
|
+
"🛡️ Running Flareguard Complete Workspace Audit...\n".bold()
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 1. Secrets Scan
|
|
367
|
+
let rules = flareguard::secrets::rules::builtin::get_builtin_rules();
|
|
368
|
+
let ignore_filter = flareguard::secrets::ignore::IgnoreFilter::new();
|
|
369
|
+
let options = flareguard::secrets::scanner::ScannerOptions {
|
|
370
|
+
max_file_size_bytes: 50 * 1024 * 1024,
|
|
371
|
+
min_severity: flareguard::secrets::rules::types::Severity::Low,
|
|
372
|
+
follow_symlinks: false,
|
|
373
|
+
};
|
|
374
|
+
let scan_res = flareguard::secrets::scanner::scan_targets(
|
|
375
|
+
std::slice::from_ref(&args.path),
|
|
376
|
+
&rules,
|
|
377
|
+
&ignore_filter,
|
|
378
|
+
&options,
|
|
379
|
+
);
|
|
380
|
+
let has_secret_leaks = !scan_res.findings.is_empty();
|
|
381
|
+
|
|
382
|
+
if args.format == CheckOutputFormat::Text {
|
|
383
|
+
println!(
|
|
384
|
+
"{}",
|
|
385
|
+
"1. Scanning for Leaked Cloudflare Secrets & Credentials..."
|
|
386
|
+
.cyan()
|
|
387
|
+
.bold()
|
|
388
|
+
);
|
|
389
|
+
if let Ok(rendered) = flareguard::secrets::report::render_report(
|
|
390
|
+
flareguard::secrets::report::OutputFormat::Text,
|
|
391
|
+
&scan_res,
|
|
392
|
+
&rules,
|
|
393
|
+
env!("CARGO_PKG_VERSION"),
|
|
394
|
+
false,
|
|
395
|
+
) {
|
|
396
|
+
println!("{}", rendered);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 2. Bindings Validation (if wrangler file found)
|
|
401
|
+
let config_path = flareguard::bindings::wrangler::find_wrangler_config(&args.path);
|
|
402
|
+
let mut has_binding_errors = false;
|
|
403
|
+
|
|
404
|
+
if let Some(ref cfg) = config_path {
|
|
405
|
+
if args.format == CheckOutputFormat::Text {
|
|
406
|
+
println!(
|
|
407
|
+
"\n{}",
|
|
408
|
+
"2. Validating Cloudflare Worker Bindings vs AST..."
|
|
409
|
+
.cyan()
|
|
410
|
+
.bold()
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if let Ok(wrangler_config) = flareguard::bindings::wrangler::parse_wrangler_config(cfg) {
|
|
414
|
+
let options = flareguard::bindings::validator::ValidatorOptions {
|
|
415
|
+
target_paths: vec![args.path.clone()],
|
|
416
|
+
environment: None,
|
|
417
|
+
ignore_unused: HashSet::new(),
|
|
418
|
+
ignore_undeclared: HashSet::new(),
|
|
419
|
+
strict: args.strict,
|
|
420
|
+
};
|
|
421
|
+
if let Ok(report) =
|
|
422
|
+
flareguard::bindings::validator::validate_project(Some(&wrangler_config), &options)
|
|
423
|
+
{
|
|
424
|
+
if args.format == CheckOutputFormat::Text {
|
|
425
|
+
let _ = flareguard::bindings::reporter::render_report(
|
|
426
|
+
&report,
|
|
427
|
+
flareguard::bindings::types::OutputFormat::Text,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
has_binding_errors = !report.undeclared_accesses.is_empty()
|
|
431
|
+
|| (args.strict && !report.ghost_bindings.is_empty());
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} else if args.format == CheckOutputFormat::Text {
|
|
435
|
+
println!(
|
|
436
|
+
"\n{} No wrangler configuration file detected in target path.",
|
|
437
|
+
"ℹ".blue()
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if args.format == CheckOutputFormat::Text {
|
|
442
|
+
if args.strict && (has_secret_leaks || has_binding_errors) {
|
|
443
|
+
println!("\n{} Workspace security check failed.", "✗".red().bold());
|
|
444
|
+
exit(1);
|
|
445
|
+
} else {
|
|
446
|
+
println!("\n{} Workspace security check passed!", "✓".green().bold());
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
Ok(())
|
|
451
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
use crate::origin::models::ConfidenceLevel;
|
|
2
|
+
use crate::origin::report::OutputFormat;
|
|
3
|
+
use clap::Parser;
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
#[derive(Parser, Debug)]
|
|
7
|
+
#[command(
|
|
8
|
+
name = "cf-origin-hunter",
|
|
9
|
+
author = "Cloudflare Security Tools Team",
|
|
10
|
+
version = "0.1.0",
|
|
11
|
+
about = "Identify unmasked backend origin IP addresses behind Cloudflare edge proxies",
|
|
12
|
+
long_about = "cf-origin-hunter is a high-performance Rust security auditing tool that discovers unmasked backend origin IPs behind Cloudflare reverse proxies using DNS SPF/MX parsing, subdomain wordlists, Certificate Transparency logs, and active HTTP/HTTPS signature verification."
|
|
13
|
+
)]
|
|
14
|
+
pub struct Cli {
|
|
15
|
+
/// Target domain to audit (e.g. example.com)
|
|
16
|
+
#[arg(value_name = "TARGET")]
|
|
17
|
+
pub target: Option<String>,
|
|
18
|
+
|
|
19
|
+
/// Run synthetic mock demonstration scan without making real network requests
|
|
20
|
+
#[arg(long, default_value_t = false)]
|
|
21
|
+
pub mock: bool,
|
|
22
|
+
|
|
23
|
+
/// CI Gate mode: Exit with code 1 if an unmasked origin IP is found matching min-confidence
|
|
24
|
+
#[arg(long, default_value_t = false)]
|
|
25
|
+
pub check: bool,
|
|
26
|
+
|
|
27
|
+
/// Minimum confidence level to report or fail on in CI gate (CONFIRMED, HIGH, MEDIUM, LOW)
|
|
28
|
+
#[arg(long, value_name = "LEVEL", default_value = "LOW")]
|
|
29
|
+
pub min_confidence: String,
|
|
30
|
+
|
|
31
|
+
/// Output format (text, json, sarif, html)
|
|
32
|
+
#[arg(long, short = 'f', value_name = "FORMAT", default_value = "text")]
|
|
33
|
+
pub format: String,
|
|
34
|
+
|
|
35
|
+
/// Save output report to specified file path
|
|
36
|
+
#[arg(long, short = 'o', value_name = "FILE")]
|
|
37
|
+
pub output: Option<PathBuf>,
|
|
38
|
+
|
|
39
|
+
/// Custom subdomain wordlist file path
|
|
40
|
+
#[arg(long, short = 'w', value_name = "FILE")]
|
|
41
|
+
pub wordlist: Option<PathBuf>,
|
|
42
|
+
|
|
43
|
+
/// Max concurrency for DNS resolution and HTTP probes
|
|
44
|
+
#[arg(long, short = 'c', value_name = "NUM", default_value_t = 10)]
|
|
45
|
+
pub concurrency: usize,
|
|
46
|
+
|
|
47
|
+
/// Network timeout in seconds for DNS and HTTP requests
|
|
48
|
+
#[arg(long, short = 't', value_name = "SECS", default_value_t = 5)]
|
|
49
|
+
pub timeout: u64,
|
|
50
|
+
|
|
51
|
+
/// Comma-separated list of ports to probe (default: 80,443,8080,8443)
|
|
52
|
+
#[arg(
|
|
53
|
+
long,
|
|
54
|
+
short = 'p',
|
|
55
|
+
value_name = "PORTS",
|
|
56
|
+
default_value = "80,443,8080,8443"
|
|
57
|
+
)]
|
|
58
|
+
pub ports: String,
|
|
59
|
+
|
|
60
|
+
/// Disable Certificate Transparency (crt.sh) log queries
|
|
61
|
+
#[arg(long, default_value_t = false)]
|
|
62
|
+
pub no_crtsh: bool,
|
|
63
|
+
|
|
64
|
+
/// Disable subdomain wordlist brute-force enumeration
|
|
65
|
+
#[arg(long, default_value_t = false)]
|
|
66
|
+
pub no_subdomains: bool,
|
|
67
|
+
|
|
68
|
+
/// Disable MX and SPF DNS record parsing
|
|
69
|
+
#[arg(long, default_value_t = false)]
|
|
70
|
+
pub no_dns: bool,
|
|
71
|
+
|
|
72
|
+
/// Enable verbose diagnostic messages
|
|
73
|
+
#[arg(long, short = 'v', default_value_t = false)]
|
|
74
|
+
pub verbose: bool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl Cli {
|
|
78
|
+
pub fn get_output_format(&self) -> OutputFormat {
|
|
79
|
+
self.format.parse().unwrap_or(OutputFormat::Text)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn get_min_confidence(&self) -> ConfidenceLevel {
|
|
83
|
+
self.min_confidence.parse().unwrap_or(ConfidenceLevel::Low)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub fn get_probe_ports(&self) -> Vec<u16> {
|
|
87
|
+
self.ports
|
|
88
|
+
.split(',')
|
|
89
|
+
.filter_map(|p| p.trim().parse::<u16>().ok())
|
|
90
|
+
.collect()
|
|
91
|
+
}
|
|
92
|
+
}
|