Skip to content

Commit 11a4891

Browse files
committed
feat: Add progress reporting with --progress flag
- Add --progress CLI flag to show progress bar during scanning - Implement progress callback system in scanner core - Add indicatif dependency for beautiful progress bars - Show file count, percentage, and findings count in real-time - Progress bar displays: [████████████████████████████████████████] 8943/10880 files (82%) | Found 8 findings - Works with parallel processing using crossbeam channels - Optional feature - no progress shown by default - Update README with new --progress flag documentation Example usage: cryptofind --progress /path/to/large/project cryptofind --patterns custom.toml --progress --threads 8 /src
1 parent 52ffe42 commit 11a4891

File tree

5 files changed

+164
-2
lines changed

5 files changed

+164
-2
lines changed

Cargo.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Key flags:
2121
- `--threads N`: set thread pool size
2222
- `--max-file-size MB`: skip large files (default 2)
2323
- `--patterns PATH`: specify patterns file (default: `patterns.toml`)
24+
- `--progress`: show progress bar during scanning
2425
- `--include-glob GLOB` / `--exclude-glob GLOB`
2526
- `--allow LIB` / `--deny LIB`
2627
- `--deterministic`: stable output ordering

crates/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ once_cell = { workspace = true }
1616
regex = { workspace = true }
1717
aho-corasick = { workspace = true }
1818
crossbeam-channel = { workspace = true }
19+
indicatif = "0.17"
1920
scanner-core = { path = "../scanner-core" }
2021

2122
[[bin]]

crates/cli/src/main.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{Context, Result};
22
use clap::{ArgAction, Parser};
3+
use indicatif::{ProgressBar, ProgressStyle};
34
use scanner_core::*;
45
use std::fs;
56
use std::path::PathBuf;
@@ -68,6 +69,10 @@ struct Args {
6869
/// Path to patterns file
6970
#[arg(long, value_name = "FILE", default_value = "patterns.toml")]
7071
patterns: PathBuf,
72+
73+
/// Show progress bar during scanning
74+
#[arg(long, action = ArgAction::SetTrue)]
75+
progress: bool,
7176
}
7277

7378
fn main() -> Result<()> {
@@ -140,6 +145,24 @@ fn main() -> Result<()> {
140145
cfg.deny_libs = args.deny.clone();
141146
cfg.deterministic = args.deterministic;
142147

148+
// Set up progress reporting if requested
149+
if args.progress {
150+
let pb = ProgressBar::new(0);
151+
pb.set_style(
152+
ProgressStyle::default_bar()
153+
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} files ({percent}%) | {msg}")
154+
.unwrap()
155+
.progress_chars("#>-"),
156+
);
157+
pb.set_message("Scanning files...");
158+
159+
cfg.progress_callback = Some(Arc::new(move |processed, total, findings| {
160+
pb.set_length(total as u64);
161+
pb.set_position(processed as u64);
162+
pb.set_message(format!("Found {} findings", findings));
163+
}));
164+
}
165+
143166
let scanner = Scanner::new(&reg, dets, cfg);
144167
if args.dry_run {
145168
let files = scanner.discover_files(&args.paths);
@@ -151,6 +174,11 @@ fn main() -> Result<()> {
151174

152175
let findings = scanner.run(&args.paths)?;
153176

177+
// Clear progress bar if it was shown
178+
if args.progress {
179+
println!(); // Move to next line after progress bar
180+
}
181+
154182
if args.json {
155183
for f in &findings {
156184
println!("{}", serde_json::to_string(f)?);

crates/scanner-core/src/lib.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub struct LibraryPatterns {
169169
pub apis: Vec<String>,
170170
}
171171

172-
#[derive(Debug, Clone, Deserialize)]
172+
#[derive(Deserialize)]
173173
pub struct Config {
174174
#[serde(default = "default_max_file_size")]
175175
pub max_file_size: usize, // bytes
@@ -185,12 +185,44 @@ pub struct Config {
185185
pub min_confidence: Option<f32>,
186186
#[serde(default)]
187187
pub deterministic: bool,
188+
#[serde(skip)]
189+
pub progress_callback: Option<Arc<dyn Fn(usize, usize, usize) + Send + Sync>>,
188190
}
189191

190192
fn default_max_file_size() -> usize {
191193
2 * 1024 * 1024
192194
}
193195

196+
impl std::fmt::Debug for Config {
197+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198+
f.debug_struct("Config")
199+
.field("max_file_size", &self.max_file_size)
200+
.field("include_globs", &self.include_globs)
201+
.field("exclude_globs", &self.exclude_globs)
202+
.field("allow_libs", &self.allow_libs)
203+
.field("deny_libs", &self.deny_libs)
204+
.field("min_confidence", &self.min_confidence)
205+
.field("deterministic", &self.deterministic)
206+
.field("progress_callback", &"<callback>")
207+
.finish()
208+
}
209+
}
210+
211+
impl Clone for Config {
212+
fn clone(&self) -> Self {
213+
Self {
214+
max_file_size: self.max_file_size,
215+
include_globs: self.include_globs.clone(),
216+
exclude_globs: self.exclude_globs.clone(),
217+
allow_libs: self.allow_libs.clone(),
218+
deny_libs: self.deny_libs.clone(),
219+
min_confidence: self.min_confidence,
220+
deterministic: self.deterministic,
221+
progress_callback: self.progress_callback.clone(),
222+
}
223+
}
224+
}
225+
194226
impl Default for Config {
195227
fn default() -> Self {
196228
Self {
@@ -201,6 +233,7 @@ impl Default for Config {
201233
deny_libs: Vec::new(),
202234
min_confidence: None,
203235
deterministic: false,
236+
progress_callback: None,
204237
}
205238
}
206239
}
@@ -791,10 +824,34 @@ impl<'a> Scanner<'a> {
791824

792825
pub fn run(&self, roots: &[PathBuf]) -> Result<Vec<Finding>> {
793826
let files = self.discover_files(roots);
827+
let total_files = files.len();
794828
let mut findings: Vec<Finding> = Vec::new();
795829

830+
// Call progress callback with initial state
831+
if let Some(ref callback) = self.config.progress_callback {
832+
callback(0, total_files, 0);
833+
}
834+
796835
let (tx, rx) = bounded::<Finding>(8192);
797-
files.par_iter().for_each_with(tx.clone(), |tx, path| {
836+
let (progress_tx, progress_rx) = bounded::<usize>(1000);
837+
838+
// Spawn a thread to collect progress updates
839+
let progress_handle = if let Some(ref callback) = self.config.progress_callback {
840+
let callback = callback.clone();
841+
Some(std::thread::spawn(move || {
842+
let mut processed = 0;
843+
let mut findings_count = 0;
844+
845+
while let Ok(_) = progress_rx.recv() {
846+
processed += 1;
847+
callback(processed, total_files, findings_count);
848+
}
849+
}))
850+
} else {
851+
None
852+
};
853+
854+
files.par_iter().for_each_with((tx.clone(), progress_tx.clone()), |(tx, progress_tx), path| {
798855
if let Some(lang) = Self::detect_language(path) {
799856
if let Ok(bytes) = Self::load_file(path) {
800857
let unit = ScanUnit {
@@ -822,13 +879,27 @@ impl<'a> Scanner<'a> {
822879
}
823880
}
824881
}
882+
// Signal that this file has been processed
883+
let _ = progress_tx.send(1);
825884
});
826885

827886
drop(tx);
887+
drop(progress_tx);
888+
828889
for f in rx.iter() {
829890
findings.push(f);
830891
}
831892

893+
// Wait for progress thread to finish
894+
if let Some(handle) = progress_handle {
895+
let _ = handle.join();
896+
}
897+
898+
// Final progress update
899+
if let Some(ref callback) = self.config.progress_callback {
900+
callback(total_files, total_files, findings.len());
901+
}
902+
832903
if self.config.deterministic {
833904
findings.sort_by(|a, b| {
834905
(

0 commit comments

Comments
 (0)