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,49 @@
1
+ use clap::{Parser, ValueEnum};
2
+ use std::path::PathBuf;
3
+
4
+ #[derive(Debug, Clone, Copy, ValueEnum)]
5
+ pub enum CliFormat {
6
+ Text,
7
+ Json,
8
+ Sarif,
9
+ }
10
+
11
+ #[derive(Parser, Debug)]
12
+ #[command(
13
+ name = "cf-binding-validator",
14
+ about = "⚡ Validate Cloudflare Worker/Pages bindings against Wrangler configuration",
15
+ version
16
+ )]
17
+ pub struct CliArgs {
18
+ /// Target files or directories to scan (defaults to current directory)
19
+ #[arg(default_value = ".")]
20
+ pub paths: Vec<PathBuf>,
21
+
22
+ /// Path to wrangler configuration (wrangler.jsonc, wrangler.json, or wrangler.toml)
23
+ #[arg(short = 'c', long = "config")]
24
+ pub config: Option<PathBuf>,
25
+
26
+ /// Wrangler environment to validate against (e.g. 'production', 'staging')
27
+ #[arg(short = 'e', long = "env")]
28
+ pub environment: Option<String>,
29
+
30
+ /// Check mode for CI/CD: exits with non-zero code if undeclared bindings exist
31
+ #[arg(long = "check")]
32
+ pub check: bool,
33
+
34
+ /// Strict mode: also fail CI if unused/ghost bindings exist
35
+ #[arg(long = "strict")]
36
+ pub strict: bool,
37
+
38
+ /// Output report format
39
+ #[arg(short = 'f', long = "format", value_enum, default_value_t = CliFormat::Text)]
40
+ pub format: CliFormat,
41
+
42
+ /// Comma-separated binding names to ignore if unused
43
+ #[arg(long = "ignore-unused", value_delimiter = ',')]
44
+ pub ignore_unused: Vec<String>,
45
+
46
+ /// Comma-separated binding names to ignore if undeclared
47
+ #[arg(long = "ignore-undeclared", value_delimiter = ',')]
48
+ pub ignore_undeclared: Vec<String>,
49
+ }
@@ -0,0 +1,166 @@
1
+ use std::error::Error;
2
+
3
+ /// Strips JavaScript-style comments (`//` and `/* ... */`) and trailing commas from JSONC text,
4
+ /// producing valid standard JSON.
5
+ pub fn clean_jsonc(input: &str) -> String {
6
+ let chars: Vec<char> = input.chars().collect();
7
+ let len = chars.len();
8
+ let mut output = String::with_capacity(len);
9
+ let mut i = 0;
10
+ let mut in_string = false;
11
+ let mut is_escaped = false;
12
+
13
+ // Step 1: Strip comments while respecting strings
14
+ let mut no_comments = String::with_capacity(len);
15
+ while i < len {
16
+ let ch = chars[i];
17
+
18
+ if in_string {
19
+ no_comments.push(ch);
20
+ if is_escaped {
21
+ is_escaped = false;
22
+ } else if ch == '\\' {
23
+ is_escaped = true;
24
+ } else if ch == '"' {
25
+ in_string = false;
26
+ }
27
+ i += 1;
28
+ continue;
29
+ }
30
+
31
+ if ch == '"' {
32
+ in_string = true;
33
+ is_escaped = false;
34
+ no_comments.push(ch);
35
+ i += 1;
36
+ continue;
37
+ }
38
+
39
+ // Check for line comment //
40
+ if ch == '/' && i + 1 < len && chars[i + 1] == '/' {
41
+ i += 2;
42
+ while i < len && chars[i] != '\n' {
43
+ i += 1;
44
+ }
45
+ if i < len {
46
+ no_comments.push('\n');
47
+ i += 1;
48
+ }
49
+ continue;
50
+ }
51
+
52
+ // Check for block comment /* ... */
53
+ if ch == '/' && i + 1 < len && chars[i + 1] == '*' {
54
+ i += 2;
55
+ while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
56
+ if chars[i] == '\n' {
57
+ no_comments.push('\n'); // Preserve line breaks for line numbering
58
+ }
59
+ i += 1;
60
+ }
61
+ i += 2; // skip */
62
+ continue;
63
+ }
64
+
65
+ no_comments.push(ch);
66
+ i += 1;
67
+ }
68
+
69
+ // Step 2: Strip trailing commas in objects and arrays
70
+ let nc_chars: Vec<char> = no_comments.chars().collect();
71
+ let nc_len = nc_chars.len();
72
+ let mut j = 0;
73
+ let mut in_str = false;
74
+ let mut esc = false;
75
+
76
+ while j < nc_len {
77
+ let ch = nc_chars[j];
78
+
79
+ if in_str {
80
+ output.push(ch);
81
+ if esc {
82
+ esc = false;
83
+ } else if ch == '\\' {
84
+ esc = true;
85
+ } else if ch == '"' {
86
+ in_str = false;
87
+ }
88
+ j += 1;
89
+ continue;
90
+ }
91
+
92
+ if ch == '"' {
93
+ in_str = true;
94
+ esc = false;
95
+ output.push(ch);
96
+ j += 1;
97
+ continue;
98
+ }
99
+
100
+ if ch == ',' {
101
+ // Peek ahead to see if the next non-whitespace character is `}` or `]`
102
+ let mut k = j + 1;
103
+ while k < nc_len && nc_chars[k].is_whitespace() {
104
+ k += 1;
105
+ }
106
+ if k < nc_len && (nc_chars[k] == '}' || nc_chars[k] == ']') {
107
+ // Skip the trailing comma
108
+ j += 1;
109
+ continue;
110
+ }
111
+ }
112
+
113
+ output.push(ch);
114
+ j += 1;
115
+ }
116
+
117
+ output
118
+ }
119
+
120
+ /// Parses JSONC or JSON text into a `serde_json::Value`.
121
+ pub fn parse_jsonc(input: &str) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
122
+ let cleaned = clean_jsonc(input);
123
+ let val: serde_json::Value = serde_json::from_str(&cleaned)?;
124
+ Ok(val)
125
+ }
126
+
127
+ #[cfg(test)]
128
+ mod tests {
129
+ use super::*;
130
+
131
+ #[test]
132
+ fn test_clean_jsonc_basic() {
133
+ let jsonc = r#"
134
+ {
135
+ // This is a line comment
136
+ "name": "my-worker", /* inline comment */
137
+ "vars": {
138
+ "API_KEY": "https://api.example.com//test", // URL with slashes
139
+ "DEBUG": true,
140
+ },
141
+ "kv_namespaces": [
142
+ {
143
+ "binding": "MY_KV",
144
+ "id": "12345",
145
+ },
146
+ ],
147
+ }
148
+ "#;
149
+ let cleaned = clean_jsonc(jsonc);
150
+ let parsed: serde_json::Value =
151
+ serde_json::from_str(&cleaned).expect("Failed to parse cleaned JSON");
152
+ assert_eq!(parsed["name"], "my-worker");
153
+ assert_eq!(parsed["vars"]["API_KEY"], "https://api.example.com//test");
154
+ assert_eq!(parsed["vars"]["DEBUG"], true);
155
+ assert_eq!(parsed["kv_namespaces"][0]["binding"], "MY_KV");
156
+ }
157
+
158
+ #[test]
159
+ fn test_clean_jsonc_nested_trailing_commas() {
160
+ let jsonc = r#"{"a": [1, 2, [3, 4,],], "b": {"c": 1,},}"#;
161
+ let cleaned = clean_jsonc(jsonc);
162
+ let parsed: serde_json::Value = serde_json::from_str(&cleaned).unwrap();
163
+ assert_eq!(parsed["a"][2][0], 3);
164
+ assert_eq!(parsed["b"]["c"], 1);
165
+ }
166
+ }
@@ -0,0 +1,7 @@
1
+ pub mod ast_scanner;
2
+ pub mod cli;
3
+ pub mod jsonc;
4
+ pub mod reporter;
5
+ pub mod types;
6
+ pub mod validator;
7
+ pub mod wrangler;
@@ -0,0 +1,328 @@
1
+ use crate::bindings::types::{OutputFormat, ValidationReport};
2
+ use colored::Colorize;
3
+ use serde_json::json;
4
+ use std::io::{self, Write};
5
+
6
+ /// Render validation report according to requested OutputFormat.
7
+ pub fn render_report(report: &ValidationReport, format: OutputFormat) -> io::Result<()> {
8
+ match format {
9
+ OutputFormat::Text => render_text_report(report),
10
+ OutputFormat::Json => render_json_report(report),
11
+ OutputFormat::Sarif => render_sarif_report(report),
12
+ }
13
+ }
14
+
15
+ /// Render rich human-readable terminal text output.
16
+ pub fn render_text_report(report: &ValidationReport) -> io::Result<()> {
17
+ let mut stdout = io::stdout().lock();
18
+
19
+ writeln!(
20
+ stdout,
21
+ "{}",
22
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
23
+ )?;
24
+ writeln!(
25
+ stdout,
26
+ "{}",
27
+ " ⚡ Cloudflare Worker / Pages Binding Validator"
28
+ .bold()
29
+ .cyan()
30
+ )?;
31
+ writeln!(
32
+ stdout,
33
+ "{}",
34
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".dimmed()
35
+ )?;
36
+
37
+ if let Some(cfg) = &report.config_file {
38
+ writeln!(stdout, " {} {}", "Config:".bold(), cfg.white())?;
39
+ } else {
40
+ writeln!(
41
+ stdout,
42
+ " {} {}",
43
+ "Config:".bold(),
44
+ "None found (all bindings treated as undeclared)".yellow()
45
+ )?;
46
+ }
47
+ writeln!(
48
+ stdout,
49
+ " {} {}",
50
+ "Environment:".bold(),
51
+ report.environment.magenta()
52
+ )?;
53
+ writeln!(
54
+ stdout,
55
+ " {} {}",
56
+ "Files Scanned:".bold(),
57
+ report.total_files_scanned.to_string().white()
58
+ )?;
59
+ writeln!(stdout)?;
60
+
61
+ // 1. Undeclared Bindings (Errors)
62
+ if !report.undeclared_accesses.is_empty() {
63
+ writeln!(
64
+ stdout,
65
+ "{}",
66
+ format!(
67
+ " CRITICAL / ERROR: Undeclared Bindings ({} found)",
68
+ report.undeclared_accesses.len()
69
+ )
70
+ .bold()
71
+ .red()
72
+ )?;
73
+ writeln!(
74
+ stdout,
75
+ " {}",
76
+ "These bindings are accessed in code but NOT declared in Wrangler config."
77
+ .red()
78
+ .italic()
79
+ )?;
80
+ writeln!(
81
+ stdout,
82
+ " {}",
83
+ "Risk: Runtime TypeError / crash when accessing undefined property at runtime."
84
+ .red()
85
+ .italic()
86
+ )?;
87
+ writeln!(stdout)?;
88
+
89
+ for access in &report.undeclared_accesses {
90
+ let loc = format!("{}:{}:{}", access.file_path, access.line, access.column);
91
+ writeln!(
92
+ stdout,
93
+ " {} {} in {}",
94
+ "✖".bold().red(),
95
+ access.name.bold().bright_red(),
96
+ loc.underline().dimmed()
97
+ )?;
98
+ writeln!(
99
+ stdout,
100
+ " {} `{}`",
101
+ "Expression:".dimmed(),
102
+ access.raw_expression.bright_yellow()
103
+ )?;
104
+ writeln!(
105
+ stdout,
106
+ " {} Declare `{}` in your wrangler configuration (e.g. `vars`, `kv_namespaces`, `d1_databases`)",
107
+ "Fix Hint:".bold().cyan(),
108
+ access.name.bright_white()
109
+ )?;
110
+ writeln!(stdout)?;
111
+ }
112
+ }
113
+
114
+ // 2. Ghost / Unused Bindings (Warnings)
115
+ if !report.ghost_bindings.is_empty() {
116
+ writeln!(
117
+ stdout,
118
+ "{}",
119
+ format!(
120
+ " WARNING: Ghost / Unused Bindings ({} found)",
121
+ report.ghost_bindings.len()
122
+ )
123
+ .bold()
124
+ .yellow()
125
+ )?;
126
+ writeln!(
127
+ stdout,
128
+ " {}",
129
+ "These bindings are declared in Wrangler config but never referenced in code."
130
+ .yellow()
131
+ .italic()
132
+ )?;
133
+ writeln!(
134
+ stdout,
135
+ " {}",
136
+ "Recommendation: Remove unused bindings to keep infrastructure lean.".dimmed()
137
+ )?;
138
+ writeln!(stdout)?;
139
+
140
+ for ghost in &report.ghost_bindings {
141
+ let details_str = ghost
142
+ .details
143
+ .as_deref()
144
+ .map(|d| format!(" ({})", d))
145
+ .unwrap_or_default();
146
+ writeln!(
147
+ stdout,
148
+ " {} {} [{}{}] in {}",
149
+ "▲".bold().yellow(),
150
+ ghost.name.bold().bright_yellow(),
151
+ ghost.binding_type,
152
+ details_str.dimmed(),
153
+ ghost.file.dimmed()
154
+ )?;
155
+ }
156
+ writeln!(stdout)?;
157
+ }
158
+
159
+ // 3. Valid / Active Bindings Summary
160
+ if !report.valid_bindings.is_empty() {
161
+ writeln!(
162
+ stdout,
163
+ "{}",
164
+ format!(
165
+ " SYNCHRONIZED BINDINGS ({} active)",
166
+ report.valid_bindings.len()
167
+ )
168
+ .bold()
169
+ .green()
170
+ )?;
171
+ for valid in &report.valid_bindings {
172
+ writeln!(
173
+ stdout,
174
+ " {} {} [{}] ({} reference{})",
175
+ "✔".bold().green(),
176
+ valid.binding.name.bold().bright_white(),
177
+ valid.binding.binding_type.to_string().dimmed(),
178
+ valid.access_count,
179
+ if valid.access_count == 1 { "" } else { "s" }
180
+ )?;
181
+ }
182
+ writeln!(stdout)?;
183
+ }
184
+
185
+ // Summary Box
186
+ writeln!(
187
+ stdout,
188
+ "{}",
189
+ "─────────────────────────────────────────────────────────────────────".dimmed()
190
+ )?;
191
+ let status_str = if report.is_success {
192
+ "✓ VALIDATION PASSED".bold().bright_green()
193
+ } else {
194
+ "✗ VALIDATION FAILED".bold().bright_red()
195
+ };
196
+
197
+ let errors_str = if report.undeclared_accesses.is_empty() {
198
+ report.undeclared_accesses.len().to_string().green()
199
+ } else {
200
+ report.undeclared_accesses.len().to_string().red().bold()
201
+ };
202
+
203
+ let warnings_str = if report.ghost_bindings.is_empty() {
204
+ report.ghost_bindings.len().to_string().green()
205
+ } else {
206
+ report.ghost_bindings.len().to_string().yellow().bold()
207
+ };
208
+
209
+ writeln!(
210
+ stdout,
211
+ " Status: {} | Declared: {} | Scanned Accesses: {} | Errors: {} | Warnings: {}",
212
+ status_str,
213
+ report.total_declared.to_string().white(),
214
+ report.total_accesses.to_string().white(),
215
+ errors_str,
216
+ warnings_str,
217
+ )?;
218
+ writeln!(
219
+ stdout,
220
+ "{}",
221
+ "─────────────────────────────────────────────────────────────────────".dimmed()
222
+ )?;
223
+
224
+ Ok(())
225
+ }
226
+
227
+ /// Render JSON format.
228
+ pub fn render_json_report(report: &ValidationReport) -> io::Result<()> {
229
+ let json_str = serde_json::to_string_pretty(report)
230
+ .map_err(|e| io::Error::other(e.to_string()))?;
231
+ println!("{}", json_str);
232
+ Ok(())
233
+ }
234
+
235
+ /// Render SARIF format (v2.1.0 standard for GitHub Actions / Code Scanning).
236
+ pub fn render_sarif_report(report: &ValidationReport) -> io::Result<()> {
237
+ let mut results = Vec::new();
238
+
239
+ // Undeclared binding errors
240
+ for access in &report.undeclared_accesses {
241
+ results.push(json!({
242
+ "ruleId": "cf001-undeclared-binding",
243
+ "level": "error",
244
+ "message": {
245
+ "text": format!("Binding '{}' accessed in code via `{}` is not declared in Wrangler config.", access.name, access.raw_expression)
246
+ },
247
+ "locations": [{
248
+ "physicalLocation": {
249
+ "artifactLocation": {
250
+ "uri": access.file_path.replace("\\", "/")
251
+ },
252
+ "region": {
253
+ "startLine": access.line,
254
+ "startColumn": access.column
255
+ }
256
+ }
257
+ }]
258
+ }));
259
+ }
260
+
261
+ // Ghost binding warnings
262
+ for ghost in &report.ghost_bindings {
263
+ results.push(json!({
264
+ "ruleId": "cf002-ghost-binding",
265
+ "level": "warning",
266
+ "message": {
267
+ "text": format!("Binding '{}' [{}] declared in '{}' is never referenced in source code.", ghost.name, ghost.binding_type, ghost.file)
268
+ },
269
+ "locations": [{
270
+ "physicalLocation": {
271
+ "artifactLocation": {
272
+ "uri": ghost.file.replace("\\", "/")
273
+ },
274
+ "region": {
275
+ "startLine": 1,
276
+ "startColumn": 1
277
+ }
278
+ }
279
+ }]
280
+ }));
281
+ }
282
+
283
+ let sarif = json!({
284
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
285
+ "version": "2.1.0",
286
+ "runs": [{
287
+ "tool": {
288
+ "driver": {
289
+ "name": "cf-binding-validator",
290
+ "informationUri": "https://github.com/cloudflare/wrangler",
291
+ "version": env!("CARGO_PKG_VERSION"),
292
+ "rules": [
293
+ {
294
+ "id": "cf001-undeclared-binding",
295
+ "name": "UndeclaredBindingAccess",
296
+ "shortDescription": {
297
+ "text": "Binding is accessed in code without being declared in Wrangler config"
298
+ },
299
+ "fullDescription": {
300
+ "text": "Accessing undefined environment bindings at runtime causes TypeErrors and Worker crashes."
301
+ },
302
+ "defaultConfiguration": {
303
+ "level": "error"
304
+ }
305
+ },
306
+ {
307
+ "id": "cf002-ghost-binding",
308
+ "name": "GhostBindingDeclared",
309
+ "shortDescription": {
310
+ "text": "Binding is declared in Wrangler configuration but never referenced in code"
311
+ },
312
+ "fullDescription": {
313
+ "text": "Unused bindings add maintenance overhead and clutter configuration."
314
+ },
315
+ "defaultConfiguration": {
316
+ "level": "warning"
317
+ }
318
+ }
319
+ ]
320
+ }
321
+ },
322
+ "results": results
323
+ }]
324
+ });
325
+
326
+ println!("{}", serde_json::to_string_pretty(&sarif).unwrap());
327
+ Ok(())
328
+ }
@@ -0,0 +1,129 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::fmt;
3
+
4
+ /// The type of Cloudflare Worker/Pages binding declared in Wrangler configuration.
5
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6
+ #[serde(rename_all = "snake_case")]
7
+ pub enum BindingType {
8
+ KvNamespace,
9
+ D1Database,
10
+ R2Bucket,
11
+ QueueProducer,
12
+ QueueConsumer,
13
+ Vectorize,
14
+ Hyperdrive,
15
+ Ai,
16
+ Service,
17
+ AnalyticsEngine,
18
+ DurableObject,
19
+ Workflow,
20
+ Browser,
21
+ SendEmail,
22
+ MtlsCertificate,
23
+ Pipeline,
24
+ Assets,
25
+ Var,
26
+ Secret,
27
+ Unknown(String),
28
+ }
29
+
30
+ impl fmt::Display for BindingType {
31
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32
+ match self {
33
+ BindingType::KvNamespace => write!(f, "KV Namespace"),
34
+ BindingType::D1Database => write!(f, "D1 Database"),
35
+ BindingType::R2Bucket => write!(f, "R2 Bucket"),
36
+ BindingType::QueueProducer => write!(f, "Queue Producer"),
37
+ BindingType::QueueConsumer => write!(f, "Queue Consumer"),
38
+ BindingType::Vectorize => write!(f, "Vectorize Index"),
39
+ BindingType::Hyperdrive => write!(f, "Hyperdrive"),
40
+ BindingType::Ai => write!(f, "Workers AI"),
41
+ BindingType::Service => write!(f, "Service Binding"),
42
+ BindingType::AnalyticsEngine => write!(f, "Analytics Engine Dataset"),
43
+ BindingType::DurableObject => write!(f, "Durable Object"),
44
+ BindingType::Workflow => write!(f, "Workflow"),
45
+ BindingType::Browser => write!(f, "Browser Rendering"),
46
+ BindingType::SendEmail => write!(f, "Send Email"),
47
+ BindingType::MtlsCertificate => write!(f, "mTLS Certificate"),
48
+ BindingType::Pipeline => write!(f, "Pipeline"),
49
+ BindingType::Assets => write!(f, "Static Assets"),
50
+ BindingType::Var => write!(f, "Environment Variable (var)"),
51
+ BindingType::Secret => write!(f, "Secret"),
52
+ BindingType::Unknown(name) => write!(f, "Custom ({})", name),
53
+ }
54
+ }
55
+ }
56
+
57
+ /// A binding declared in `wrangler.jsonc`, `wrangler.json`, or `wrangler.toml`.
58
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59
+ pub struct DeclaredBinding {
60
+ pub name: String,
61
+ pub binding_type: BindingType,
62
+ pub file: String,
63
+ pub environment: String,
64
+ pub details: Option<String>,
65
+ }
66
+
67
+ /// How a binding was accessed in source code.
68
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69
+ #[serde(rename_all = "snake_case")]
70
+ pub enum AccessKind {
71
+ DirectMember,
72
+ Destructured,
73
+ ParamDestructured,
74
+ HelperCall,
75
+ ProcessEnv,
76
+ }
77
+
78
+ impl fmt::Display for AccessKind {
79
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80
+ match self {
81
+ AccessKind::DirectMember => write!(f, "property access"),
82
+ AccessKind::Destructured => write!(f, "destructuring"),
83
+ AccessKind::ParamDestructured => write!(f, "parameter destructuring"),
84
+ AccessKind::HelperCall => write!(f, "helper call"),
85
+ AccessKind::ProcessEnv => write!(f, "process.env access"),
86
+ }
87
+ }
88
+ }
89
+
90
+ /// An instance in source code where a binding was accessed.
91
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92
+ pub struct BindingAccess {
93
+ pub name: String,
94
+ pub file_path: String,
95
+ pub line: usize,
96
+ pub column: usize,
97
+ pub raw_expression: String,
98
+ pub access_kind: AccessKind,
99
+ }
100
+
101
+ /// Information about a binding that was successfully matched between config and code.
102
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103
+ pub struct ValidBindingInfo {
104
+ pub binding: DeclaredBinding,
105
+ pub access_count: usize,
106
+ pub accesses: Vec<BindingAccess>,
107
+ }
108
+
109
+ /// Complete report of the validation process.
110
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111
+ pub struct ValidationReport {
112
+ pub config_file: Option<String>,
113
+ pub environment: String,
114
+ pub total_files_scanned: usize,
115
+ pub total_declared: usize,
116
+ pub total_accesses: usize,
117
+ pub undeclared_accesses: Vec<BindingAccess>,
118
+ pub ghost_bindings: Vec<DeclaredBinding>,
119
+ pub valid_bindings: Vec<ValidBindingInfo>,
120
+ pub is_success: bool,
121
+ }
122
+
123
+ /// Output formats supported by the CLI.
124
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
125
+ pub enum OutputFormat {
126
+ Text,
127
+ Json,
128
+ Sarif,
129
+ }