@masyv/secretscan 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/LICENSE +21 -0
- package/README.md +122 -0
- package/core/Cargo.toml +51 -0
- package/core/src/hook/mod.rs +100 -0
- package/core/src/lib.rs +93 -0
- package/core/src/main.rs +481 -0
- package/core/src/patterns/builtin.rs +366 -0
- package/core/src/patterns/entropy.rs +129 -0
- package/core/src/patterns/mod.rs +83 -0
- package/core/src/redact/mod.rs +69 -0
- package/core/src/store/mod.rs +241 -0
- package/hooks/scan-output.sh +37 -0
- package/package.json +37 -0
- package/plugin/tool.json +50 -0
- package/scripts/build.sh +9 -0
- package/scripts/install.sh +24 -0
package/core/src/main.rs
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use clap::{Parser, Subcommand};
|
|
3
|
+
use colored::Colorize;
|
|
4
|
+
use std::io::Read;
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
|
|
7
|
+
use secretscan::{patterns, redact, store::Store};
|
|
8
|
+
|
|
9
|
+
#[derive(Parser)]
|
|
10
|
+
#[command(
|
|
11
|
+
name = "secretscan",
|
|
12
|
+
about = "SecretScan — Real-time secret & credential detector for Claude Code",
|
|
13
|
+
long_about = "Scans text, files, and Claude Code tool outputs for API keys, tokens,\nprivate keys, database URLs, and 50+ other secret formats.\n\nDetected secrets are redacted with [REDACTED:type:fingerprint] placeholders.\nOriginals are stored locally in SQLite for recovery — never sent anywhere.",
|
|
14
|
+
version
|
|
15
|
+
)]
|
|
16
|
+
struct Cli {
|
|
17
|
+
#[command(subcommand)]
|
|
18
|
+
command: Commands,
|
|
19
|
+
|
|
20
|
+
/// Output as JSON
|
|
21
|
+
#[arg(long, global = true)]
|
|
22
|
+
json: bool,
|
|
23
|
+
|
|
24
|
+
/// Verbose logging
|
|
25
|
+
#[arg(long, short, global = true)]
|
|
26
|
+
verbose: bool,
|
|
27
|
+
|
|
28
|
+
/// SQLite database path
|
|
29
|
+
#[arg(long, global = true, default_value = "~/.secretscan/secretscan.db")]
|
|
30
|
+
db_path: String,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Subcommand)]
|
|
34
|
+
enum Commands {
|
|
35
|
+
/// Scan text, a file, or stdin for secrets
|
|
36
|
+
Scan {
|
|
37
|
+
/// File path or '-' for stdin
|
|
38
|
+
#[arg(default_value = "-")]
|
|
39
|
+
input: String,
|
|
40
|
+
|
|
41
|
+
/// Redact detected secrets in the output
|
|
42
|
+
#[arg(long, short)]
|
|
43
|
+
redact: bool,
|
|
44
|
+
|
|
45
|
+
/// Minimum severity to report (low, medium, high, critical)
|
|
46
|
+
#[arg(long, default_value = "low")]
|
|
47
|
+
severity: String,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/// PostToolUse / PreToolUse hook mode (reads hook JSON from stdin)
|
|
51
|
+
Hook {
|
|
52
|
+
/// Session ID
|
|
53
|
+
#[arg(long, default_value = "unknown")]
|
|
54
|
+
session: String,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/// Expand a redacted secret by fingerprint (local recovery only)
|
|
58
|
+
Expand {
|
|
59
|
+
/// Fingerprint from [REDACTED:type:FINGERPRINT]
|
|
60
|
+
fingerprint: String,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/// Add a fingerprint to the allowlist (mark as safe / not a secret)
|
|
64
|
+
Allow {
|
|
65
|
+
/// Fingerprint to allowlist
|
|
66
|
+
fingerprint: String,
|
|
67
|
+
|
|
68
|
+
/// Reason for allowlisting
|
|
69
|
+
#[arg(long, short)]
|
|
70
|
+
reason: Option<String>,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/// Remove a fingerprint from the allowlist
|
|
74
|
+
Unallow {
|
|
75
|
+
/// Fingerprint to remove
|
|
76
|
+
fingerprint: String,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/// Show scan statistics
|
|
80
|
+
Stats,
|
|
81
|
+
|
|
82
|
+
/// List recent findings
|
|
83
|
+
Audit {
|
|
84
|
+
/// Number of findings to show
|
|
85
|
+
#[arg(long, default_value = "20")]
|
|
86
|
+
limit: usize,
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/// List all built-in patterns
|
|
90
|
+
Patterns,
|
|
91
|
+
|
|
92
|
+
/// Auto-install hook into ~/.claude/settings.json
|
|
93
|
+
Setup {
|
|
94
|
+
/// Preview without writing
|
|
95
|
+
#[arg(long)]
|
|
96
|
+
dry_run: bool,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn main() -> Result<()> {
|
|
101
|
+
let cli = Cli::parse();
|
|
102
|
+
|
|
103
|
+
let filter = if cli.verbose { "debug" } else { "warn" };
|
|
104
|
+
tracing_subscriber::fmt()
|
|
105
|
+
.with_env_filter(
|
|
106
|
+
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
107
|
+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)),
|
|
108
|
+
)
|
|
109
|
+
.with_target(false)
|
|
110
|
+
.with_writer(std::io::stderr)
|
|
111
|
+
.init();
|
|
112
|
+
|
|
113
|
+
let db_path = resolve_db_path(&cli.db_path)?;
|
|
114
|
+
|
|
115
|
+
match cli.command {
|
|
116
|
+
Commands::Scan { input, redact, severity } => {
|
|
117
|
+
let text = read_input(&input)?;
|
|
118
|
+
let min_sev = parse_severity(&severity);
|
|
119
|
+
|
|
120
|
+
let findings = patterns::scan_all(&text);
|
|
121
|
+
let filtered: Vec<_> = findings
|
|
122
|
+
.iter()
|
|
123
|
+
.filter(|f| f.severity >= min_sev)
|
|
124
|
+
.collect();
|
|
125
|
+
|
|
126
|
+
if cli.json {
|
|
127
|
+
println!("{}", serde_json::to_string_pretty(&filtered)?);
|
|
128
|
+
return Ok(());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if filtered.is_empty() {
|
|
132
|
+
println!("{}", "✅ No secrets detected.".green());
|
|
133
|
+
return Ok(());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
eprintln!(
|
|
137
|
+
"{}",
|
|
138
|
+
format!("🚨 {} secret{} found:", filtered.len(),
|
|
139
|
+
if filtered.len() == 1 { "" } else { "s" }).red().bold()
|
|
140
|
+
);
|
|
141
|
+
eprintln!();
|
|
142
|
+
for f in &filtered {
|
|
143
|
+
eprintln!(
|
|
144
|
+
" {} {:8} {:<40} fingerprint: {}",
|
|
145
|
+
f.severity.emoji(),
|
|
146
|
+
format!("[{}]", f.severity.as_str().to_uppercase())
|
|
147
|
+
.bold(),
|
|
148
|
+
f.pattern_name,
|
|
149
|
+
f.fingerprint.dimmed()
|
|
150
|
+
);
|
|
151
|
+
let preview = if f.matched.len() > 40 {
|
|
152
|
+
format!("{}…", &f.matched[..37])
|
|
153
|
+
} else {
|
|
154
|
+
f.matched.clone()
|
|
155
|
+
};
|
|
156
|
+
eprintln!(" {}", preview.dimmed());
|
|
157
|
+
eprintln!();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if redact {
|
|
161
|
+
let result = redact::scan_and_redact(&text, None, &db_path, "cli");
|
|
162
|
+
print!("{}", result.redacted_text);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Exit code 1 if any High/Critical found
|
|
166
|
+
let has_critical = filtered.iter().any(|f| {
|
|
167
|
+
f.severity >= secretscan::Severity::High
|
|
168
|
+
});
|
|
169
|
+
if has_critical {
|
|
170
|
+
std::process::exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Commands::Hook { session } => {
|
|
175
|
+
let mut raw = String::new();
|
|
176
|
+
std::io::stdin().read_to_string(&mut raw)?;
|
|
177
|
+
|
|
178
|
+
let result = secretscan::hook::process(&raw, &db_path, &session)?;
|
|
179
|
+
print!("{result}");
|
|
180
|
+
|
|
181
|
+
// Log to stderr if secrets were found
|
|
182
|
+
if let Ok(output) = serde_json::from_str::<serde_json::Value>(&result) {
|
|
183
|
+
if let Some(meta) = output.get("secretscan") {
|
|
184
|
+
let found = meta.get("secrets_found")
|
|
185
|
+
.and_then(|v| v.as_u64()).unwrap_or(0);
|
|
186
|
+
if found > 0 {
|
|
187
|
+
eprintln!(
|
|
188
|
+
"[secretscan] 🚨 {} secret{} redacted from {} output",
|
|
189
|
+
found,
|
|
190
|
+
if found == 1 { "" } else { "s" },
|
|
191
|
+
output.get("tool_name")
|
|
192
|
+
.and_then(|v| v.as_str())
|
|
193
|
+
.unwrap_or("tool")
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Commands::Expand { fingerprint } => {
|
|
201
|
+
let store = Store::open(&db_path)?;
|
|
202
|
+
match store.get_original(&fingerprint)? {
|
|
203
|
+
Some(original) => {
|
|
204
|
+
if cli.json {
|
|
205
|
+
println!("{}", serde_json::json!({ "fingerprint": fingerprint, "original": original }));
|
|
206
|
+
} else {
|
|
207
|
+
println!("{original}");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
None => {
|
|
211
|
+
eprintln!("No record found for fingerprint: {fingerprint}");
|
|
212
|
+
std::process::exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Commands::Allow { fingerprint, reason } => {
|
|
218
|
+
let store = Store::open(&db_path)?;
|
|
219
|
+
store.allow(&fingerprint, reason.as_deref())?;
|
|
220
|
+
if cli.json {
|
|
221
|
+
println!("{}", serde_json::json!({ "allowed": fingerprint, "reason": reason }));
|
|
222
|
+
} else {
|
|
223
|
+
println!("✅ {} added to allowlist.", fingerprint.green());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Commands::Unallow { fingerprint } => {
|
|
228
|
+
let store = Store::open(&db_path)?;
|
|
229
|
+
let removed = store.unallow(&fingerprint)?;
|
|
230
|
+
if cli.json {
|
|
231
|
+
println!("{}", serde_json::json!({ "removed": removed, "fingerprint": fingerprint }));
|
|
232
|
+
} else if removed {
|
|
233
|
+
println!("✅ {} removed from allowlist.", fingerprint.green());
|
|
234
|
+
} else {
|
|
235
|
+
println!("ℹ️ {} was not on the allowlist.", fingerprint);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Commands::Stats => {
|
|
240
|
+
let store = Store::open(&db_path)?;
|
|
241
|
+
let stats = store.stats()?;
|
|
242
|
+
|
|
243
|
+
if cli.json {
|
|
244
|
+
println!("{}", serde_json::to_string_pretty(&stats)?);
|
|
245
|
+
} else {
|
|
246
|
+
println!("{}", "SecretScan Statistics".bold());
|
|
247
|
+
println!("{}", "─".repeat(45));
|
|
248
|
+
println!(" Total scans: {}", stats.total_scans);
|
|
249
|
+
println!(" Bytes scanned: {}", format_bytes(stats.total_bytes_scanned));
|
|
250
|
+
println!(" Secrets found: {}", stats.total_secrets_found);
|
|
251
|
+
println!(" Secrets redacted: {}", stats.total_secrets_redacted);
|
|
252
|
+
println!(" Unique secrets seen: {}", stats.unique_secrets);
|
|
253
|
+
println!(" Allowlist entries: {}", stats.allowlist_count);
|
|
254
|
+
if !stats.by_severity.is_empty() {
|
|
255
|
+
println!();
|
|
256
|
+
println!(" By severity:");
|
|
257
|
+
for (sev, count) in &stats.by_severity {
|
|
258
|
+
println!(" {:<10} {}", sev, count);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Commands::Audit { limit } => {
|
|
265
|
+
let store = Store::open(&db_path)?;
|
|
266
|
+
let findings = store.recent_findings(limit)?;
|
|
267
|
+
|
|
268
|
+
if cli.json {
|
|
269
|
+
println!("{}", serde_json::to_string_pretty(&findings)?);
|
|
270
|
+
return Ok(());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if findings.is_empty() {
|
|
274
|
+
println!("No findings recorded yet.");
|
|
275
|
+
return Ok(());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
println!("{}", format!("Recent {} findings:", findings.len()).bold());
|
|
279
|
+
println!("{}", "─".repeat(70));
|
|
280
|
+
for f in &findings {
|
|
281
|
+
println!(
|
|
282
|
+
" {} {:<12} {:<35} {}",
|
|
283
|
+
f.fingerprint.dimmed(),
|
|
284
|
+
f.severity.bold(),
|
|
285
|
+
f.pattern_name,
|
|
286
|
+
f.detected_at.dimmed()
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Commands::Patterns => {
|
|
292
|
+
if cli.json {
|
|
293
|
+
let list: Vec<_> = patterns::PATTERNS.iter().map(|p| {
|
|
294
|
+
serde_json::json!({
|
|
295
|
+
"id": p.id,
|
|
296
|
+
"name": p.name,
|
|
297
|
+
"severity": p.severity.as_str()
|
|
298
|
+
})
|
|
299
|
+
}).collect();
|
|
300
|
+
println!("{}", serde_json::to_string_pretty(&list)?);
|
|
301
|
+
return Ok(());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
println!("{}", format!("Built-in patterns ({}):", patterns::PATTERNS.len()).bold());
|
|
305
|
+
println!("{}", "─".repeat(65));
|
|
306
|
+
for p in patterns::PATTERNS.iter() {
|
|
307
|
+
println!(
|
|
308
|
+
" {} {:8} {:<20} {}",
|
|
309
|
+
p.severity.emoji(),
|
|
310
|
+
format!("[{}]", p.severity.as_str().to_uppercase()),
|
|
311
|
+
p.id,
|
|
312
|
+
p.name
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
Commands::Setup { dry_run } => {
|
|
318
|
+
run_setup(dry_run, &cli.json)?;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
Ok(())
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Setup ────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
fn run_setup(dry_run: bool, json: &bool) -> Result<()> {
|
|
328
|
+
let settings_path = {
|
|
329
|
+
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
330
|
+
PathBuf::from(home).join(".claude/settings.json")
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
let binary = std::env::current_exe()
|
|
334
|
+
.ok()
|
|
335
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
336
|
+
.unwrap_or_else(|| "secretscan".to_string());
|
|
337
|
+
|
|
338
|
+
let hook_command = format!("{binary} hook --session ${{CLAUDE_SESSION_ID:-default}}");
|
|
339
|
+
|
|
340
|
+
// Read existing settings
|
|
341
|
+
let mut settings = if settings_path.exists() {
|
|
342
|
+
let data = std::fs::read_to_string(&settings_path)?;
|
|
343
|
+
serde_json::from_str::<serde_json::Value>(&data).unwrap_or(serde_json::json!({}))
|
|
344
|
+
} else {
|
|
345
|
+
serde_json::json!({})
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Check if already configured
|
|
349
|
+
let already = is_already_configured(&settings);
|
|
350
|
+
|
|
351
|
+
if already {
|
|
352
|
+
let msg = "SecretScan hook already configured — nothing to do.";
|
|
353
|
+
if *json {
|
|
354
|
+
println!("{}", serde_json::json!({ "already_configured": true, "message": msg }));
|
|
355
|
+
} else {
|
|
356
|
+
println!("✅ {msg}");
|
|
357
|
+
}
|
|
358
|
+
return Ok(());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if !dry_run {
|
|
362
|
+
// Inject hook
|
|
363
|
+
let hook_entry = serde_json::json!({
|
|
364
|
+
"matcher": "*",
|
|
365
|
+
"hooks": [{ "type": "command", "command": hook_command }]
|
|
366
|
+
});
|
|
367
|
+
let hooks = settings
|
|
368
|
+
.as_object_mut()
|
|
369
|
+
.unwrap()
|
|
370
|
+
.entry("hooks")
|
|
371
|
+
.or_insert(serde_json::json!({}));
|
|
372
|
+
let ptu = hooks
|
|
373
|
+
.as_object_mut()
|
|
374
|
+
.unwrap()
|
|
375
|
+
.entry("PostToolUse")
|
|
376
|
+
.or_insert(serde_json::json!([]));
|
|
377
|
+
if let Some(arr) = ptu.as_array_mut() {
|
|
378
|
+
arr.push(hook_entry);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Atomic write
|
|
382
|
+
if let Some(parent) = settings_path.parent() {
|
|
383
|
+
std::fs::create_dir_all(parent)?;
|
|
384
|
+
}
|
|
385
|
+
let tmp = settings_path.with_extension("json.tmp");
|
|
386
|
+
std::fs::write(&tmp, serde_json::to_string_pretty(&settings)?)?;
|
|
387
|
+
std::fs::rename(&tmp, &settings_path)?;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let message = if dry_run {
|
|
391
|
+
format!("[DRY RUN] Would add PostToolUse hook to {}", settings_path.display())
|
|
392
|
+
} else {
|
|
393
|
+
format!(
|
|
394
|
+
"PostToolUse hook added to {}.\nRestart Claude Code to activate.",
|
|
395
|
+
settings_path.display()
|
|
396
|
+
)
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if *json {
|
|
400
|
+
println!("{}", serde_json::json!({
|
|
401
|
+
"configured": !dry_run,
|
|
402
|
+
"dry_run": dry_run,
|
|
403
|
+
"settings_path": settings_path,
|
|
404
|
+
"hook_command": hook_command,
|
|
405
|
+
"message": message
|
|
406
|
+
}));
|
|
407
|
+
} else {
|
|
408
|
+
println!("✅ {message}");
|
|
409
|
+
println!();
|
|
410
|
+
println!(" Settings: {}", settings_path.display());
|
|
411
|
+
println!(" Command: {hook_command}");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
Ok(())
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn is_already_configured(settings: &serde_json::Value) -> bool {
|
|
418
|
+
let Some(hooks) = settings.get("hooks") else { return false };
|
|
419
|
+
let Some(ptu) = hooks.get("PostToolUse") else { return false };
|
|
420
|
+
let Some(arr) = ptu.as_array() else { return false };
|
|
421
|
+
arr.iter().any(|entry| {
|
|
422
|
+
entry.get("hooks")
|
|
423
|
+
.and_then(|h| h.as_array())
|
|
424
|
+
.map(|hs| hs.iter().any(|h| {
|
|
425
|
+
h.get("command")
|
|
426
|
+
.and_then(|c| c.as_str())
|
|
427
|
+
.map(|c| c.contains("secretscan"))
|
|
428
|
+
.unwrap_or(false)
|
|
429
|
+
}))
|
|
430
|
+
.unwrap_or(false)
|
|
431
|
+
|| entry.get("command")
|
|
432
|
+
.and_then(|c| c.as_str())
|
|
433
|
+
.map(|c| c.contains("secretscan"))
|
|
434
|
+
.unwrap_or(false)
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
fn resolve_db_path(raw: &str) -> Result<PathBuf> {
|
|
441
|
+
let expanded = if raw.starts_with("~/") {
|
|
442
|
+
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
443
|
+
PathBuf::from(home).join(&raw[2..])
|
|
444
|
+
} else {
|
|
445
|
+
PathBuf::from(raw)
|
|
446
|
+
};
|
|
447
|
+
if let Some(parent) = expanded.parent() {
|
|
448
|
+
std::fs::create_dir_all(parent)?;
|
|
449
|
+
}
|
|
450
|
+
Ok(expanded)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fn read_input(input: &str) -> Result<String> {
|
|
454
|
+
if input == "-" {
|
|
455
|
+
let mut buf = String::new();
|
|
456
|
+
std::io::stdin().read_to_string(&mut buf)?;
|
|
457
|
+
Ok(buf)
|
|
458
|
+
} else {
|
|
459
|
+
std::fs::read_to_string(input)
|
|
460
|
+
.map_err(|e| anyhow::anyhow!("failed to read {input}: {e}"))
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fn parse_severity(s: &str) -> secretscan::Severity {
|
|
465
|
+
match s.to_lowercase().as_str() {
|
|
466
|
+
"medium" | "med" => secretscan::Severity::Medium,
|
|
467
|
+
"high" => secretscan::Severity::High,
|
|
468
|
+
"critical" => secretscan::Severity::Critical,
|
|
469
|
+
_ => secretscan::Severity::Low,
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fn format_bytes(bytes: usize) -> String {
|
|
474
|
+
if bytes < 1024 {
|
|
475
|
+
format!("{bytes} B")
|
|
476
|
+
} else if bytes < 1024 * 1024 {
|
|
477
|
+
format!("{:.1} KB", bytes as f64 / 1024.0)
|
|
478
|
+
} else {
|
|
479
|
+
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
|
|
480
|
+
}
|
|
481
|
+
}
|