Skip to content
Closed
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
31 changes: 31 additions & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,34 @@ is closed — see [CONTRIBUTING.md](CONTRIBUTING.md).
- **By:** ALLEN-AYODEJI
- **Closes:** N/A
- **Status:** Merged.

### frontend — Open Graph and social preview tags

- **What:** Added `og:*` / Twitter card meta tags to `frontend/index.html` and
generated `frontend/public/og-image.png` (1200×630) from `assets/banner.svg`.
- **By:** kevindurant735rocket-creator
- **Closes:** #169
- **Status:** `npm run lint`, `typecheck`, `test` (21 tests) and `build` pass.

### examples — vulnerable-app demo fixture

- **What:** Added `examples/vulnerable-app/` with a minimal npm project whose
lockfile pins `lodash@4.17.4` and whose manifest also lists `lodashz`, a
non-existent, typosquat-looking package name. README documents expected
DepRadar findings (CVE for old lodash, typosquat of lodash, missing
resolution) plus verification commands.
- **By:** kevindurant735rocket-creator
- **Closes:** #174
- **Status:** Fixture verified; `package-lock.json` resolves `lodash@4.17.4`
and `cargo run --bin scan -- lodashz` reports the typosquat finding.

### typosquat — popularity rank for matches

- **What:** Exposed `typosquat::dataset::rank(name) -> Option<usize>` over the
bundled popular-package lists, and carried the matched package's rank in
`ScanResult` (serialized as `rank`) plus the scanner's JSON output. Added
`--json` to the `scan` binary.
- **By:** kevindurant735rocket-creator
- **Closes:** #177
- **Status:** `cargo fmt`, `cargo clippy --workspace --all-targets --locked -- -D
warnings`, and `cargo test --workspace --locked` pass.
35 changes: 31 additions & 4 deletions src/bin/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,48 @@
//! Usage: `scan <package-name>` — scores the given name against the bundled
//! popular-package dataset with both the edit-distance and homoglyph signals
//! and prints the result.
//!
//! Pass `--json` before the name to print the result as JSON.

use std::env;
use std::process::ExitCode;

use deprader::typosquat::scanner::score_package;

fn main() -> ExitCode {
let name = match env::args().nth(1) {
let args = env::args().skip(1);
let mut json_output = false;
let mut name: Option<String> = None;

for arg in args {
if arg == "--json" {
json_output = true;
} else if name.is_none() {
name = Some(arg);
} else {
eprintln!("usage: scan [--json] <package-name>");
return ExitCode::FAILURE;
}
}

let name = match name {
Some(name) => name,
None => {
eprintln!("usage: scan <package-name>");
eprintln!("usage: scan [--json] <package-name>");
return ExitCode::FAILURE;
}
};

println!("{}", score_package(&name));
let result = score_package(&name);
if json_output {
match serde_json::to_string_pretty(&result) {
Ok(text) => println!("{text}"),
Err(err) => {
eprintln!("failed to render JSON: {err}");
return ExitCode::FAILURE;
}
}
} else {
println!("{}", result);
}
ExitCode::SUCCESS
}
13 changes: 13 additions & 0 deletions src/typosquat/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ fn parse_string_array(raw: &str) -> Vec<String> {
out
}

/// Returns the zero-based position (rank) of `name` in the combined popular
/// package dataset, or `None` if it is not present.
///
/// The dataset is already ordered by popularity within each registry (npm
/// entries first, then PyPI), so a lower number means a more popular (and
/// more dangerous to typosquat) target. `load_popular_packages` defines that
/// order.
pub fn rank(name: &str) -> Option<usize> {
load_popular_packages()
.iter()
.position(|entry| entry == name)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
21 changes: 18 additions & 3 deletions src/typosquat/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

use std::fmt;

use serde::Serialize;

use super::dataset::load_popular_packages;
use super::distance::nearest_match;
use super::homoglyph::homoglyph_impersonation;
Expand All @@ -24,7 +26,7 @@ pub const DISTANCE_THRESHOLD: usize = 2;

/// Result of scoring a candidate package name against both typosquat
/// signals.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ScanResult {
/// The name that was scored, as given by the caller.
pub candidate: String,
Expand All @@ -36,6 +38,9 @@ pub struct ScanResult {
/// lookalike characters are folded back to Latin, is a popular package:
/// the matched name.
pub homoglyph_match: Option<String>,
/// Popularity rank (lower is more popular) of the popular package matched
/// by either signal, when one fired. `None` when no signal fired.
pub rank: Option<usize>,
}

impl ScanResult {
Expand Down Expand Up @@ -76,15 +81,25 @@ pub fn score_package(name: &str) -> ScanResult {
let dataset = load_popular_packages();

let (nearest_name, distance) = nearest_match(name, &dataset);
let distance_match =
(distance > 0 && distance <= DISTANCE_THRESHOLD).then_some((nearest_name, distance));
let distance_match = (distance > 0 && distance <= DISTANCE_THRESHOLD)
.then_some((nearest_name.clone(), distance));

let homoglyph_match = homoglyph_impersonation(name, &dataset);

let rank = distance_match
.as_ref()
.and_then(|(matched, _)| super::dataset::rank(matched))
.or_else(|| {
homoglyph_match
.as_ref()
.and_then(|matched| super::dataset::rank(matched))
});

ScanResult {
candidate: name.to_string(),
distance_match,
homoglyph_match,
rank,
}
}

Expand Down