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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 0.2.0

- `cargo capdiff snapshot` writes a `capdiff.lock` snapshot of the current
capability sets, for use as a reviewed diff baseline.
- `cargo capdiff explain <crate>` deep-dives on one crate with severity rationale,
dependency path, and full evidence.
- Findings now carry the dependency path that pulled the crate in, shown as
`(via a -> b -> crate)` in human output and `dep_path` in JSON.
- `capdiff.toml` in the project root suppresses known-acceptable findings and
overrides severity per crate. `--no-config` ignores it.
- Severity tags are colorized in human output (red High, yellow Notable, dim
Info), auto-disabled on non-TTY and when `NO_COLOR` is set.
- JSON schemas bumped: audit version 5, diff version 4. Both add
`suppressed_count` (top-level) and optional `dep_path` (per finding).

## 0.1.0

First release.
Expand Down
34 changes: 30 additions & 4 deletions 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 @@ -4,7 +4,7 @@ members = ["crates/capdiff-core", "crates/cargo-capdiff"]

[workspace.package]
edition = "2024"
version = "0.1.0"
version = "0.2.0"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
repository = "https://github.com/telcharr/capdiff"
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ Audit the whole tree with no baseline:
cargo capdiff audit
```

Diff against a reviewed snapshot instead of git HEAD:
Write a reviewed snapshot of the current capabilities, then diff against it later:

```
cargo capdiff snapshot # writes capdiff.lock
cargo capdiff --baseline capdiff.lock
```

Dig into one crate to see why it was flagged, what it does, and how it entered your tree:

```
cargo capdiff explain ring
```

### Output formats

`--format human` (default), `--format json`, or `--format sarif`. JSON conforms to
Expand All @@ -57,9 +64,10 @@ code-scanning. JSON audit output:

```json
{
"version": 4,
"version": 5,
"crate_count": 1,
"skipped_count": 0,
"suppressed_count": 0,
"findings": [
{
"crate": "badnet",
Expand All @@ -83,14 +91,34 @@ code-scanning. JSON audit output:
### Flags

- `--format {human|json|sarif}`
- `--fail-on {none|notable|high}` -- exit `1` when a finding reaches the threshold
- `--fail-on {none|notable|high}`: exit `1` when a finding reaches the threshold
(default `high` for diff, `none` for audit)
- `--baseline <file>` -- diff against a committed `capdiff.lock` snapshot
- `--strict` -- exit `2` if any source file could not be parsed
- `--no-cache` -- do not persist the fetch/extract cache
- `--baseline <file>`: diff against a committed `capdiff.lock` snapshot
- `--strict`: exit `2` if any source file could not be parsed
- `--no-cache`: do not persist the fetch/extract cache
- `--no-config`: ignore `capdiff.toml`

Exit codes: `0` clean, `1` findings at or above `--fail-on`, `2` usage, IO, or strict failure.

### Suppressing known findings

Drop a `capdiff.toml` next to your `Cargo.toml` to silence findings you have
reviewed and accepted, or to pin a crate's severity:

```toml
[[suppress]]
crate = "ring"
version = ">=0.17, <0.18" # optional; omit to match all versions
reason = "vendored asm, reviewed"

[[override]]
crate = "some-build-tool"
severity = "Notable"
```

Suppressed findings leave the report but the count is still shown, so nothing
disappears silently.

## What it looks for

Each dependency version fingerprints to a small capability set:
Expand Down
42 changes: 38 additions & 4 deletions crates/capdiff-core/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::Serialize;

use crate::cache::Cache;
use crate::capability::Capability;
use crate::config::Config;
use crate::fingerprint::Evidence;
use crate::severity::Severity;

Expand All @@ -15,6 +16,7 @@ pub struct AuditReport {
pub version: u32,
pub crate_count: usize,
pub skipped_count: usize,
pub suppressed_count: usize,
pub findings: Vec<AuditFinding>,
}

Expand All @@ -28,6 +30,8 @@ pub struct AuditFinding {
pub evidence: Vec<Evidence>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub skipped_files: Vec<PathBuf>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dep_path: Vec<String>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -76,7 +80,15 @@ impl From<crate::fingerprint::FingerprintError> for AuditError {
}

pub fn audit_project(project_root: &Path, cache: &Cache) -> Result<AuditReport, AuditError> {
let resolved = crate::resolve::resolve_lockfile(project_root)?;
audit_project_with_config(project_root, cache, &Config::default())
}

pub fn audit_project_with_config(
project_root: &Path,
cache: &Cache,
config: &Config,
) -> Result<AuditReport, AuditError> {
let (resolved, dep_paths) = crate::resolve::resolve_and_paths(project_root)?;
let crate_count = resolved.len();

let findings: Result<Vec<AuditFinding>, AuditError> = resolved
Expand All @@ -85,6 +97,18 @@ pub fn audit_project(project_root: &Path, cache: &Cache) -> Result<AuditReport,
.collect();
let mut findings = findings?;

for f in &mut findings {
if let Some(path) = dep_paths.get(&(f.krate.clone(), f.version.clone())) {
f.dep_path = path.clone();
}
}

for f in &mut findings {
if let Some(sev) = config.severity_override(&f.krate, &f.version) {
f.severity = sev;
}
}

findings.sort_by(|a, b| {
b.severity
.cmp(&a.severity)
Expand All @@ -94,10 +118,17 @@ pub fn audit_project(project_root: &Path, cache: &Cache) -> Result<AuditReport,

let skipped_count = findings.iter().map(|f| f.skipped_files.len()).sum();

let suppressed_count = findings
.iter()
.filter(|f| config.is_suppressed(&f.krate, &f.version))
.count();
findings.retain(|f| !config.is_suppressed(&f.krate, &f.version));

Ok(AuditReport {
version: 4,
version: 5,
crate_count,
skipped_count,
suppressed_count,
findings,
})
}
Expand Down Expand Up @@ -130,6 +161,7 @@ fn fingerprint_one(
severity,
evidence: fp.evidence,
skipped_files: fp.skipped_files,
dep_path: Vec::new(),
}))
}

Expand All @@ -140,15 +172,16 @@ mod tests {
#[test]
fn report_is_serializable() {
let report = AuditReport {
version: 4,
version: 5,
crate_count: 0,
skipped_count: 0,
suppressed_count: 0,
findings: vec![],
};
let v = serde_json::to_value(&report).unwrap();
assert_eq!(
v,
serde_json::json!({"version": 4, "crate_count": 0, "skipped_count": 0, "findings": []})
serde_json::json!({"version": 5, "crate_count": 0, "skipped_count": 0, "suppressed_count": 0, "findings": []})
);
}

Expand All @@ -161,6 +194,7 @@ mod tests {
severity: crate::severity::Severity::Info,
evidence: vec![],
skipped_files: vec![],
dep_path: vec![],
};
let v = serde_json::to_value(&f).unwrap();
assert_eq!(v["crate"], "foo");
Expand Down
Loading
Loading