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
20 changes: 20 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ http-body-util = "0.1"
url = "2.5"
open = "5.0"
urlencoding = "2.1"
serde_yaml_ng = "0.10"

[target.'cfg(not(target_os = "windows"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ Once the binary is installed, login with your token from the Corgea app.
corgea login <token>
```

## Dependency Inventory (offline)

`corgea deps` builds a dependency inventory from npm, Python, and Java manifests
and lockfiles, then evaluates a pinning policy (DEP rules). Runs fully offline —
no token or network required.

```bash
corgea deps scan # table report for the current directory
corgea deps scan --fail-on high # exit 1 if any finding is >= high
corgea deps scan --out-format json # machine-readable (json or sarif)
corgea deps graph # print the resolved dependency graph
corgea deps explain <package> # show why a package is present
corgea deps sbom --format cyclonedx # emit a CycloneDX SBOM
corgea deps policy init # write a starter .corgea/deps.yml
```

See [Dependency Scanning (CLI)](https://docs.corgea.app/cli/deps) for the full flag and exit-code reference.

## Development Setup

Expand Down
103 changes: 103 additions & 0 deletions src/deps/detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::path::{Path, PathBuf};

use crate::deps::model::Ecosystem;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DepFileKind {
NpmManifest,
NpmLockfile,
YarnLockfile,
PnpmLockfile,
PipRequirements,
PipConstraints,
PyProject,
PoetryLock,
UvLock,
MavenPom,
GradleBuild,
GradleLockfile,
GoMod,
GoSum,
CargoManifest,
CargoLock,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedFile {
pub path: PathBuf,
pub kind: DepFileKind,
pub ecosystem: Ecosystem,
}

const SKIP_DIRS: &[&str] = &[
"node_modules",
".git",
"vendor",
"target",
".venv",
"venv",
"__pycache__",
"dist",
"build",
];

/// Recursively detect supported dependency files; skip vendored/VCS dirs.
pub fn detect_dependency_files(root: &Path) -> Vec<DetectedFile> {
let mut out = Vec::new();
detect_recursive(root, &mut out);
out.sort_by(|a, b| a.path.cmp(&b.path));
out
}

fn detect_recursive(dir: &Path, out: &mut Vec<DetectedFile>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};

for entry in entries.flatten() {
let path = entry.path();
let file_name = entry.file_name();
let name = file_name.to_string_lossy();

if path.is_dir() {
if SKIP_DIRS.iter().any(|s| name == *s) {
continue;
}
detect_recursive(&path, out);
continue;
}

if let Some(detected) = classify_file(&path) {
out.push(detected);
}
}
}

fn classify_file(path: &Path) -> Option<DetectedFile> {
let name = path.file_name()?.to_string_lossy();
let kind_eco = match name.as_ref() {
"package.json" => (DepFileKind::NpmManifest, Ecosystem::Npm),
"package-lock.json" | "npm-shrinkwrap.json" => (DepFileKind::NpmLockfile, Ecosystem::Npm),
"yarn.lock" => (DepFileKind::YarnLockfile, Ecosystem::Npm),
"pnpm-lock.yaml" => (DepFileKind::PnpmLockfile, Ecosystem::Npm),
"requirements.txt" => (DepFileKind::PipRequirements, Ecosystem::PyPI),
"constraints.txt" => (DepFileKind::PipConstraints, Ecosystem::PyPI),
"pyproject.toml" => (DepFileKind::PyProject, Ecosystem::PyPI),
"poetry.lock" => (DepFileKind::PoetryLock, Ecosystem::PyPI),
"uv.lock" => (DepFileKind::UvLock, Ecosystem::PyPI),
"pom.xml" => (DepFileKind::MavenPom, Ecosystem::Maven),
"build.gradle" | "build.gradle.kts" => (DepFileKind::GradleBuild, Ecosystem::Maven),
"gradle.lockfile" => (DepFileKind::GradleLockfile, Ecosystem::Maven),
"go.mod" => (DepFileKind::GoMod, Ecosystem::Go),
"go.sum" => (DepFileKind::GoSum, Ecosystem::Go),
"Cargo.toml" => (DepFileKind::CargoManifest, Ecosystem::Cargo),
"Cargo.lock" => (DepFileKind::CargoLock, Ecosystem::Cargo),
_ => return None,
};
Some(DetectedFile {
path: path.to_path_buf(),
kind: kind_eco.0,
ecosystem: kind_eco.1,
})
}
63 changes: 63 additions & 0 deletions src/deps/diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::deps::model::{DependencyGraph, DependencyNode};

#[derive(Debug)]
pub struct VersionChange {
pub name: String,
pub from: String,
pub to: String,
}

#[derive(Debug)]
pub struct GraphDiff {
pub added: Vec<DependencyNode>,
pub removed: Vec<DependencyNode>,
pub changed: Vec<VersionChange>,
}

pub fn diff_graphs(base: &DependencyGraph, head: &DependencyGraph) -> GraphDiff {
let mut base_map: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for n in &base.nodes {
if let Some(v) = &n.version {
base_map.insert(n.name.clone(), v.clone());
}
}
let mut head_map: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for n in &head.nodes {
if let Some(v) = &n.version {
head_map.insert(n.name.clone(), v.clone());
}
}

let mut added = Vec::new();
let mut changed = Vec::new();
for n in &head.nodes {
match base_map.get(&n.name) {
None => added.push(n.clone()),
Some(old) if n.version.as_deref() != Some(old.as_str()) => {
if let Some(new_v) = &n.version {
changed.push(VersionChange {
name: n.name.clone(),
from: old.clone(),
to: new_v.clone(),
});
}
}
_ => {}
}
}

let mut removed = Vec::new();
for n in &base.nodes {
if !head_map.contains_key(&n.name) {
removed.push(n.clone());
}
}

GraphDiff {
added,
removed,
changed,
}
}
Loading
Loading