Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to Diskern are documented here. The format follows

### Added

- `diskern scan --rules <file>` to test scans with an external rules database
- Cancel a running scan from the desktop app
- `diskern scan` prints the findings themselves — grouped by verdict and
category, with `--top` to cap each group and `--verdict` to filter
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/diskern-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ diskern-core = { path = "../diskern-core" }
anyhow.workspace = true
serde_json.workspace = true
clap = { version = "4", features = ["derive"] }

[dev-dependencies]
tempfile = "3"
31 changes: 29 additions & 2 deletions crates/diskern-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use diskern_core::{report, rules::RulesDb, scanner, Category, Finding, Verdict};
use std::path::PathBuf;
Expand Down Expand Up @@ -29,6 +29,9 @@ enum Command {
/// Only show findings with this verdict
#[arg(long, value_enum)]
verdict: Option<VerdictFilter>,
/// Load rules from a JSON file instead of the embedded database
#[arg(long, value_name = "FILE")]
rules: Option<PathBuf>,
},
}

Expand Down Expand Up @@ -183,6 +186,21 @@ fn print_findings(findings: &[&Finding], top: usize) {
}
}

fn load_rules(path: Option<&std::path::Path>) -> Result<RulesDb> {
let Some(path) = path else {
return Ok(RulesDb::embedded());
};

let contents = std::fs::read(path).with_context(|| {
format!(
"could not read rules file '{}'; check that it exists and is readable",
path.display()
)
})?;
serde_json::from_slice(&contents)
.with_context(|| format!("could not parse rules file '{}' as JSON", path.display()))
}

fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Expand All @@ -191,19 +209,28 @@ fn main() -> Result<()> {
json,
top,
verdict,
rules,
} => {
let external_rules = rules.as_deref();
let rules_db = load_rules(external_rules)?;
let opts = scanner::ScanOptions {
roots,
..Default::default()
};
let progress = Arc::new(scanner::ScanProgress::default());
let entries = scanner::scan(&opts, progress)?;
let report = report::build(entries, &RulesDb::embedded());
let report = report::build(entries, &rules_db);

if json {
if let Some(path) = external_rules {
eprintln!("Using external rules database: {}", path.display());
}
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
println!("Scanned {} files.", report.files_scanned);
if let Some(path) = external_rules {
println!("Rules: external database — {}", path.display());
}
println!(
"Reclaimable: {} across {} findings and {} duplicate sets.",
human_bytes(report.total_reclaimable),
Expand Down
91 changes: 91 additions & 0 deletions crates/diskern-cli/tests/rules_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use serde_json::json;
use std::fs;
use std::process::Command;
use tempfile::tempdir;

fn run_scan(root: &std::path::Path, rules: Option<&std::path::Path>) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_diskern"));
command.arg("scan").arg(root).arg("--top").arg("0");
if let Some(rules) = rules {
command.arg("--rules").arg(rules);
}
command.output().expect("diskern should start")
}

fn write_rules(path: &std::path::Path, pattern: &str) {
let rules = json!({
"version": 1,
"rules": [{
"id": "test-rule",
"patterns": [pattern],
"category": "browser_cache",
"verdict": "safe",
"description": "Rule loaded from the test file."
}]
});
fs::write(path, serde_json::to_vec(&rules).unwrap()).unwrap();
}

#[test]
fn scan_without_rules_uses_embedded_database() {
let root = tempdir().unwrap();
let cache = root.path().join(".cache/google-chrome/Default/Cache");
fs::create_dir_all(&cache).unwrap();
fs::write(cache.join("entry"), b"cached").unwrap();

let output = run_scan(root.path(), None);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("matched rule chrome-cache"), "{stdout}");
}

#[test]
fn scan_accepts_and_uses_external_rules_file() {
let root = tempdir().unwrap();
fs::write(root.path().join("sample.custom"), b"custom").unwrap();
let rules = root.path().join("rules.json");
write_rules(&rules, "**/*.custom");

let output = run_scan(root.path(), Some(&rules));
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Rules: external database"), "{stdout}");
assert!(stdout.contains("matched rule test-rule"), "{stdout}");
assert!(
stdout.contains("Rule loaded from the test file."),
"{stdout}"
);
}

#[test]
fn missing_rules_file_fails_clearly() {
let root = tempdir().unwrap();
let missing = root.path().join("missing.json");
let output = run_scan(root.path(), Some(&missing));

assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("could not read rules file"), "{stderr}");
assert!(stderr.contains("missing.json"), "{stderr}");
}

#[test]
fn malformed_rules_file_fails_clearly() {
let root = tempdir().unwrap();
let rules = root.path().join("malformed.json");
fs::write(&rules, b"{ not json }").unwrap();
let output = run_scan(root.path(), Some(&rules));

assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("could not parse rules file"), "{stderr}");
assert!(stderr.contains("malformed.json"), "{stderr}");
}