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,227 @@
1
+ use crate::bindings::ast_scanner::scan_source_file;
2
+ use crate::bindings::types::{BindingAccess, DeclaredBinding, ValidBindingInfo, ValidationReport};
3
+ use crate::bindings::wrangler::WranglerConfig;
4
+ use std::collections::{HashMap, HashSet};
5
+ use std::error::Error;
6
+ use std::path::PathBuf;
7
+ use walkdir::WalkDir;
8
+
9
+ /// Configuration options for the validator.
10
+ #[derive(Debug, Clone)]
11
+ pub struct ValidatorOptions {
12
+ pub target_paths: Vec<PathBuf>,
13
+ pub environment: Option<String>,
14
+ pub ignore_unused: HashSet<String>,
15
+ pub ignore_undeclared: HashSet<String>,
16
+ pub strict: bool,
17
+ }
18
+
19
+ impl Default for ValidatorOptions {
20
+ fn default() -> Self {
21
+ Self {
22
+ target_paths: vec![PathBuf::from(".")],
23
+ environment: None,
24
+ ignore_unused: HashSet::new(),
25
+ ignore_undeclared: HashSet::new(),
26
+ strict: false,
27
+ }
28
+ }
29
+ }
30
+
31
+ /// Discovers all candidate source files in the given paths.
32
+ pub fn discover_source_files(paths: &[PathBuf]) -> Vec<PathBuf> {
33
+ let mut files = Vec::new();
34
+ let ignored_dirs = [
35
+ "node_modules",
36
+ ".git",
37
+ "target",
38
+ "dist",
39
+ "build",
40
+ ".wrangler",
41
+ ".next",
42
+ ".astro",
43
+ ".cache",
44
+ "coverage",
45
+ ".output",
46
+ "vendor",
47
+ ".turbo",
48
+ ".svelte-kit",
49
+ ];
50
+
51
+ let supported_exts = ["ts", "tsx", "js", "jsx", "mjs", "cjs", "astro"];
52
+
53
+ for p in paths {
54
+ if p.is_file() {
55
+ if let Some(ext) = p.extension().and_then(|e| e.to_str())
56
+ && supported_exts.contains(&ext) {
57
+ files.push(p.clone());
58
+ }
59
+ continue;
60
+ }
61
+
62
+ if p.is_dir() {
63
+ for entry in WalkDir::new(p)
64
+ .follow_links(false)
65
+ .into_iter()
66
+ .filter_entry(|e| {
67
+ let name = e.file_name().to_string_lossy();
68
+ if e.file_type().is_dir() {
69
+ !ignored_dirs.contains(&name.as_ref())
70
+ } else {
71
+ true
72
+ }
73
+ })
74
+ .filter_map(|e| e.ok())
75
+ {
76
+ if entry.file_type().is_file() {
77
+ let path = entry.path();
78
+ if let Some(ext) = path.extension().and_then(|e| e.to_str())
79
+ && supported_exts.contains(&ext) {
80
+ files.push(path.to_path_buf());
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ files.sort();
88
+ files
89
+ }
90
+
91
+ /// Runs validation of code binding accesses against declared Wrangler bindings.
92
+ pub fn validate_project(
93
+ wrangler_config: Option<&WranglerConfig>,
94
+ options: &ValidatorOptions,
95
+ ) -> Result<ValidationReport, Box<dyn Error + Send + Sync>> {
96
+ let env_name = options.environment.as_deref().unwrap_or("root");
97
+
98
+ // 1. Get declared bindings from Wrangler config
99
+ let declared_bindings = if let Some(cfg) = wrangler_config {
100
+ cfg.get_bindings_for_env(options.environment.as_deref())?
101
+ } else {
102
+ Vec::new()
103
+ };
104
+
105
+ let declared_map: HashMap<String, DeclaredBinding> = declared_bindings
106
+ .iter()
107
+ .map(|b| (b.name.clone(), b.clone()))
108
+ .collect();
109
+
110
+ // 2. Discover and scan source files
111
+ let source_files = discover_source_files(&options.target_paths);
112
+ let mut all_accesses = Vec::new();
113
+
114
+ for file_path in &source_files {
115
+ if let Ok(accesses) = scan_source_file(file_path) {
116
+ all_accesses.extend(accesses);
117
+ }
118
+ }
119
+
120
+ // 3. Group accesses by binding name
121
+ let mut accesses_by_name: HashMap<String, Vec<BindingAccess>> = HashMap::new();
122
+ for access in all_accesses.clone() {
123
+ accesses_by_name
124
+ .entry(access.name.clone())
125
+ .or_default()
126
+ .push(access);
127
+ }
128
+
129
+ // 4. Cross-reference
130
+ let mut undeclared_accesses = Vec::new();
131
+ let mut ghost_bindings = Vec::new();
132
+ let mut valid_bindings = Vec::new();
133
+
134
+ // Check accesses for undeclared bindings
135
+ for (name, accesses) in &accesses_by_name {
136
+ if options.ignore_undeclared.contains(name) {
137
+ continue;
138
+ }
139
+
140
+ if !declared_map.contains_key(name) {
141
+ // All accesses to this undeclared name are errors
142
+ undeclared_accesses.extend(accesses.clone());
143
+ }
144
+ }
145
+
146
+ // Check declared bindings for unused (ghost) bindings
147
+ for declared in &declared_bindings {
148
+ if options.ignore_unused.contains(&declared.name) {
149
+ continue;
150
+ }
151
+
152
+ if let Some(accesses) = accesses_by_name.get(&declared.name) {
153
+ valid_bindings.push(ValidBindingInfo {
154
+ binding: declared.clone(),
155
+ access_count: accesses.len(),
156
+ accesses: accesses.clone(),
157
+ });
158
+ } else {
159
+ // Declared but never accessed in any scanned source file
160
+ ghost_bindings.push(declared.clone());
161
+ }
162
+ }
163
+
164
+ // Sort for deterministic results
165
+ undeclared_accesses
166
+ .sort_by(|a, b| (&a.file_path, a.line, a.column).cmp(&(&b.file_path, b.line, b.column)));
167
+ ghost_bindings.sort_by(|a, b| a.name.cmp(&b.name));
168
+ valid_bindings.sort_by(|a, b| a.binding.name.cmp(&b.binding.name));
169
+
170
+ let is_success = if options.strict {
171
+ undeclared_accesses.is_empty() && ghost_bindings.is_empty()
172
+ } else {
173
+ undeclared_accesses.is_empty()
174
+ };
175
+
176
+ Ok(ValidationReport {
177
+ config_file: wrangler_config.map(|c| c.file_path.to_string_lossy().to_string()),
178
+ environment: env_name.to_string(),
179
+ total_files_scanned: source_files.len(),
180
+ total_declared: declared_bindings.len(),
181
+ total_accesses: all_accesses.len(),
182
+ undeclared_accesses,
183
+ ghost_bindings,
184
+ valid_bindings,
185
+ is_success,
186
+ })
187
+ }
188
+
189
+ #[cfg(test)]
190
+ mod tests {
191
+ use super::*;
192
+ use crate::bindings::types::BindingType;
193
+
194
+ #[test]
195
+ fn test_cross_referencing_logic() {
196
+ let root_bindings = vec![
197
+ DeclaredBinding {
198
+ name: "MY_KV".to_string(),
199
+ binding_type: BindingType::KvNamespace,
200
+ file: "wrangler.jsonc".to_string(),
201
+ environment: "root".to_string(),
202
+ details: None,
203
+ },
204
+ DeclaredBinding {
205
+ name: "UNUSED_GHOST_DB".to_string(),
206
+ binding_type: BindingType::D1Database,
207
+ file: "wrangler.jsonc".to_string(),
208
+ environment: "root".to_string(),
209
+ details: None,
210
+ },
211
+ ];
212
+
213
+ let config = WranglerConfig {
214
+ file_path: PathBuf::from("wrangler.jsonc"),
215
+ root_bindings,
216
+ environments: std::collections::BTreeMap::new(),
217
+ };
218
+
219
+ let opts = ValidatorOptions {
220
+ target_paths: vec![],
221
+ ..Default::default()
222
+ };
223
+
224
+ let report = validate_project(Some(&config), &opts).unwrap();
225
+ assert_eq!(report.ghost_bindings.len(), 2);
226
+ }
227
+ }