Skip to content
Merged
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
55 changes: 54 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async = []
base64 = "0.22"
clap = { version = "4", features = ["derive"] }
dirs = "5"
flate2 = "1"
indicatif = "0.17"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
sha2 = "0.10"
Expand Down
134 changes: 14 additions & 120 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,124 +505,18 @@ is closed — see [CONTRIBUTING.md](CONTRIBUTING.md).
- **Closes:** #82
- **Status:** In review.

### CLI — project config file (`depradar.toml`)

- **What:** Adds `src/config.rs` with a `ProjectConfig` struct that `depradar
scan` loads from `depradar.toml` in the working directory. The file supports
four keys:
- `fail_on` — severity threshold for a non-zero exit code
(`low`/`medium`/`high`/`critical`/`none`; default `high`).
- `format` — output format (`text`/`json`; default `text`).
- `exclude` — array of glob patterns for lockfile paths to skip.
- `ecosystems` — array of ecosystems to restrict scanning to (`npm`,
`pypi`, `cargo`, `go`, `maven`, `rubygems`, `packagist`; default: all).

Loading is strictly opt-in: if no `depradar.toml` exists the built-in
defaults are unchanged. An existing but malformed file exits with code 2 and
a clear error pointing to the file. Unknown keys are rejected.

Precedence is: **built-in defaults ← `depradar.toml` ← CLI flags**. CLI
flags always win, so existing scripts and one-off commands are unaffected.
The `--fail-on`, `--exclude`, and `--ecosystem` flags are new on the CLI
(previously undocumented/unimplemented), and `--format` is now optional
rather than defaulting on the struct level, giving the file a chance to
provide the default.

- **Tests:**
- Unit tests in `src/config.rs`: `FailOn` ordering, round-trip
`FromStr`/`Display` for all three enums, full-config TOML parse,
empty-file parse, unknown-key rejection, invalid enum value rejection,
`load_project_config` returning `None` when absent, loading from a
directory, and error on malformed TOML.
- Integration tests in `tests/cli.rs`: config file format setting used
when no flag is passed; CLI `--format` overrides the file; `fail_on =
"none"` exits 0 with findings; `--fail-on high` overrides `fail_on =
"none"` in the file; `exclude` pattern skips a path; CLI `--exclude`
overrides the file; no config file uses defaults; invalid config exits 2
with a message mentioning `depradar.toml`.
- `cargo fmt`, `cargo clippy -- -D warnings`, and `cargo test --workspace`
all pass.
- **Docs:** README updated with a `depradar.toml` section, a key reference
table, and an updated CLI options table reflecting the newly implemented
flags.
- **By:** david-tanko
- **Closes:** #85
- **Status:** Merged.

### behavioral — safe npm tarball download and unpack

- **What:** Adds `src/behavioral/tarball.rs`. `download_npm_dist` downloads
`dist.tarball` through the shared polite HTTP client, caps the body at
50 MB, and verifies `dist.integrity` as a SHA-512 SRI digest before
returning any bytes (`verify_integrity` accepts several SRI tokens and
requires at least one matching `sha512-` value). `unpack_tarball`
decompresses a gzipped npm tarball into memory, never writing a
path taken from the archive and never executing anything. `UnpackLimits`
caps total unpacked bytes (50 MB), per-file bytes, entry count (10,000)
and path length (1 KB); absolute paths, `..`, backslashes, drive prefixes,
NULs, non-UTF-8 paths, duplicate paths, symlinks, hard links and all other
non-file/non-directory entry types are rejected. The npm `package/` prefix
is stripped only when every entry is under it. Returns sorted
`PackageFile { path, data }` values.
- **Tests:** crafted tar.gz archives for path traversal, absolute paths,
Windows-style paths, symlinks, hard links, size bombs, total-size caps,
file-count caps, duplicates, non-UTF-8/overlong paths and corrupt gzip;
SHA-512 verification (match, tamper, missing, malformed, multi-token) and
mocked download size/integrity checks.
- **By:** ushpraise
- **Closes:** #100
- **Status:** In review.

### behavioral — npm install-script detector

- **What:** Adds `src/behavioral/scripts.rs`.
`install_scripts(package_json) -> Vec<(String, String)>` parses
`package.json` and returns `preinstall`, `install`, `postinstall` and
`prepare` commands in lifecycle order; invalid JSON, unrelated scripts and
non-string values are ignored. `InstallScript::finding` wraps each one as a
medium-severity `BehavioralFinding` on `package.json`, and
`install_script_findings` returns them all. Nothing is executed.
- **Tests:** no scripts, one script, all four hooks (with unrelated entries
mixed in), non-string values, and the finding message/severity.
- **By:** ushpraise
- **Closes:** #99
- **Status:** In review.

### behavioral — obfuscated JavaScript heuristics

- **What:** Adds `src/behavioral/obfuscation.rs`. `score_javascript` scores a
file on base64 runs of 200+ characters (3 points), hex runs of 200+
characters (4), `eval(` (2), `new Function(` (3), `atob(` (2),
high-entropy 128+ character string literals (2 each, max 4) and source
lines over 2,000 characters (1). `OBFUSCATION_THRESHOLD` is 4, so a
minified bundle's single long-line point is not enough; scores of 8+ are
high severity, 4–7 medium. Markers are counted only in code, with comments
and string contents masked. `scan_obfuscation` emits the finding and
`scan_tree` walks `.js`/`.mjs`/`.cjs` files (5 MB per-file cap).
- **Tests:** a minified-but-benign file is not flagged; an `atob` + base64 +
`eval` sample is flagged high; hex blobs, short blobs, markers in
strings/comments, lookalike identifiers, long-line-only files and tree
scanning.
- **By:** ushpraise
- **Closes:** #101
- **Status:** In review.

### behavioral — network and credential access in install scripts

- **What:** Adds `src/behavioral/network.rs`. `scan_script` line-scans a
command or invoked script for `curl`/`wget` piped to
`sh`/`bash`/`zsh`/`dash`/`fish` (including `sh -c "$(curl ...)"`; high),
HTTP(S) requests to raw IPv4 literals (medium), and reads of `NPM_TOKEN`,
`AWS_*`, `GITHUB_TOKEN`/`GH_TOKEN`, `~/.ssh` and `~/.npmrc` (high).
Findings include a line number and a snippet truncated to 80 characters.
`scan_install_scripts` labels findings with the package.json hook, and
`scan_package` follows local script files named by lifecycle commands
(`node scripts/install.js`, `sh ./setup.sh`), refusing absolute or `..`
paths.
- **Tests:** one per pattern (curl pipe, wget pipe, raw-IP request, npm/AWS/
GitHub tokens, SSH/npmrc access), a benign build script that must not flag,
snippet truncation, and following invoked files without escaping the
package root.
- **By:** ushpraise
- **Closes:** #102
### CLI — progress bar for OSV vulnerability checks

- **What:** Adds a progress bar on stderr when checking packages against the
OSV API. Uses `indicatif` (already in `Cargo.toml`) to show
`Checking for vulnerabilities [###>---] 42/100 packages` during the
advisories stage in [`pipeline.rs`](src/pipeline.rs). The bar appears only
when stderr is a TTY and not in `--quiet` mode (`ScanOptions::show_progress`).
Non-TTY stderr (pipes, CI) sees no progress output. The bar clears itself
on completion so it doesn't clutter the final report.
- **Tests:** Manual verification — progress appears when running
`depradar scan` on a lockfile with many dependencies, and disappears with
`--quiet` or when piped.
- **By:** coderolisa
- **Closes:** Part of an issue (1% contribution)
- **Status:** In review.
65 changes: 21 additions & 44 deletions src/bin/depradar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ struct Cli {
#[arg(long, value_enum, global = true)]
fail_on: Option<FailOnArg>,

/// Suppress progress indicators and non-essential output
#[arg(long, short = 'q', global = true)]
quiet: bool,

#[command(subcommand)]
command: Command,
}
Expand Down Expand Up @@ -217,43 +221,18 @@ fn main() -> ExitCode {
};

match cli.command {
Command::Name { package } => {
// `name` doesn't use fail_on / exclude / ecosystems — resolve
// format only.
let format = cli.format.unwrap_or_else(|| {
project_config
.as_ref()
.and_then(|f| f.format)
.map(config_format_to_cli)
.unwrap_or(Format::Text)
});
run_name(&package, format)
}
Command::Scan {
path,
offline,
exclude,
ecosystems,
} => {
let resolved = ResolvedConfig::resolve(
project_config.as_ref(),
cli.format,
cli.fail_on,
exclude,
ecosystems,
);
match scan(&path, offline, &resolved.exclude, &resolved.ecosystems) {
Ok(scanned) => {
match (resolved.format, &scanned) {
(Format::Text, Scanned::File(report)) => println!("{report}"),
(Format::Text, Scanned::Dir(report)) => println!("{report}"),
(Format::Json, scanned) => print_json(&scanned_json(scanned)),
}
if scanned_exceeds_threshold(&scanned, resolved.fail_on) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
Command::Name { package } => run_name(&package, cli.format),
Command::Scan { path, offline } => match scan(&path, offline, cli.quiet) {
Ok(scanned) => {
match (cli.format, &scanned) {
(Format::Text, Scanned::File(report)) => println!("{report}"),
(Format::Text, Scanned::Dir(report)) => println!("{report}"),
(Format::Json, scanned) => print_json(&scanned_json(scanned)),
}
if scanned.has_findings() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
Err(message) => {
eprintln!("error: {message}");
Expand Down Expand Up @@ -431,13 +410,8 @@ fn glob_match_inner(pattern: &[u8], text: &[u8]) -> bool {

/// Scans `path` as a single lockfile or, for a directory, every lockfile
/// under it, honouring a `.depradarignore` next to the lockfile (or at the
/// directory's root) and any `--exclude` glob patterns.
fn scan(
path: &Path,
offline: bool,
exclude: &[String],
_ecosystems: &[String],
) -> Result<Scanned, String> {
/// directory's root).
fn scan(path: &Path, offline: bool, quiet: bool) -> Result<Scanned, String> {
let ignore_dir = if path.is_dir() {
path
} else {
Expand Down Expand Up @@ -467,6 +441,9 @@ fn scan(
if offline {
opts.advisories = None;
}
if quiet {
opts.show_progress = false;
}

if path.is_dir() {
let mut report =
Expand Down
Loading