Skip to content

Commit 5da1ddd

Browse files
authored
Merge pull request #23 from bordumb/dev-ecosystem
feat: ecosystem integration — SARIF fixes, check-deny, badge, audit report
2 parents 7cfb6ed + 29e164d commit 5da1ddd

7 files changed

Lines changed: 707 additions & 36 deletions

File tree

‎crates/cargo-capsec/bench/audit_wild.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,22 @@ def parse_crates_toml(path: Path) -> list[dict]:
6161

6262
def get_capsec_version() -> str:
6363
"""Get the installed cargo-capsec version."""
64+
# Try --version first (most reliable)
65+
result = subprocess.run(
66+
["cargo", "capsec", "--version"],
67+
capture_output=True,
68+
text=True,
69+
)
70+
if result.returncode == 0 and result.stdout.strip():
71+
return result.stdout.strip()
72+
# Fall back to parsing help output
6473
result = subprocess.run(
6574
["cargo", "capsec", "audit", "--help"],
6675
capture_output=True,
6776
text=True,
6877
)
69-
# Try to extract from help output or just return unknown
7078
for line in result.stdout.splitlines():
71-
if "version" in line.lower():
79+
if "version" in line.lower() or "capsec" in line.lower():
7280
return line.strip()
7381
return "unknown"
7482

@@ -97,6 +105,20 @@ def clone_repo(repo_url: str, dest: Path) -> bool:
97105
return True
98106

99107

108+
def strip_temp_paths(audit_data: dict | None, crate_dir: Path) -> dict | None:
109+
"""Strip absolute temp directory prefix from file paths in audit output."""
110+
if not audit_data:
111+
return audit_data
112+
prefix = str(crate_dir)
113+
if not prefix.endswith("/"):
114+
prefix += "/"
115+
for crate_entry in audit_data.get("crates", []):
116+
for finding in crate_entry.get("findings", []):
117+
if "file" in finding and finding["file"].startswith(prefix):
118+
finding["file"] = finding["file"][len(prefix):]
119+
return audit_data
120+
121+
100122
def run_audit(crate_dir: Path) -> dict:
101123
"""Run cargo capsec audit on a directory. Returns parsed result."""
102124
start = time.monotonic()
@@ -117,7 +139,8 @@ def run_audit(crate_dir: Path) -> dict:
117139

118140
if result.stdout.strip():
119141
try:
120-
output["audit"] = json.loads(result.stdout)
142+
audit = json.loads(result.stdout)
143+
output["audit"] = strip_temp_paths(audit, crate_dir)
121144
except json.JSONDecodeError:
122145
output["audit"] = None
123146
output["parse_error"] = "Failed to parse JSON output"

‎crates/cargo-capsec/src/cli.rs‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ use std::path::PathBuf;
1313
cargo capsec audit --format sarif SARIF for GitHub Code Scanning\n \
1414
cargo capsec audit --baseline Save results as baseline\n \
1515
cargo capsec audit --diff Show changes since last baseline\n \
16-
cargo capsec audit --min-risk high Only show high and critical findings"
16+
cargo capsec audit --min-risk high Only show high and critical findings\n \
17+
cargo capsec check-deny Verify #[capsec::deny] annotations\n \
18+
cargo capsec badge Generate shields.io badge\n \
19+
cargo capsec badge --json Output shields.io endpoint JSON"
1720
)]
1821
pub struct Cli {
1922
/// When invoked as `cargo capsec`, cargo passes "capsec" as the first arg.
@@ -36,6 +39,10 @@ pub enum CargoSubcommand {
3639
pub enum Commands {
3740
/// Scan for ambient authority usage
3841
Audit(AuditArgs),
42+
/// Verify #[capsec::deny] annotations are respected
43+
CheckDeny(CheckDenyArgs),
44+
/// Generate a shields.io badge from audit results
45+
Badge(BadgeArgs),
3946
}
4047

4148
#[derive(clap::Args)]
@@ -80,3 +87,37 @@ pub struct AuditArgs {
8087
#[arg(short, long)]
8188
pub quiet: bool,
8289
}
90+
91+
#[derive(clap::Args)]
92+
pub struct CheckDenyArgs {
93+
/// Path to workspace root
94+
#[arg(short, long, default_value = ".")]
95+
pub path: PathBuf,
96+
97+
/// Output format
98+
#[arg(short, long, default_value = "text", value_parser = ["text", "json", "sarif"])]
99+
pub format: String,
100+
101+
/// Only scan these crates (comma-separated)
102+
#[arg(long)]
103+
pub only: Option<String>,
104+
105+
/// Skip these crates (comma-separated)
106+
#[arg(long)]
107+
pub skip: Option<String>,
108+
}
109+
110+
#[derive(clap::Args)]
111+
pub struct BadgeArgs {
112+
/// Path to workspace root
113+
#[arg(short, long, default_value = ".")]
114+
pub path: PathBuf,
115+
116+
/// Output shields.io endpoint JSON instead of markdown
117+
#[arg(long)]
118+
pub json: bool,
119+
120+
/// Risk threshold for badge color (default: high)
121+
#[arg(long, default_value = "high", value_parser = ["low", "medium", "high", "critical"])]
122+
pub fail_on: String,
123+
}

‎crates/cargo-capsec/src/discovery.rs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub struct CrateInfo {
2828
#[derive(Deserialize)]
2929
struct CargoMetadata {
3030
packages: Vec<Package>,
31+
workspace_root: String,
3132
}
3233

3334
#[derive(Deserialize)]
@@ -38,17 +39,27 @@ struct Package {
3839
source: Option<String>,
3940
}
4041

42+
/// Result of workspace discovery: crates and the resolved workspace root.
43+
pub struct DiscoveryResult {
44+
/// All discovered crates.
45+
pub crates: Vec<CrateInfo>,
46+
/// The Cargo workspace root (from `cargo metadata`).
47+
pub workspace_root: PathBuf,
48+
}
49+
4150
/// Discovers all crates in a Cargo workspace by running `cargo metadata`.
4251
///
4352
/// When `include_deps` is `false` (default), passes `--no-deps` for speed — only
4453
/// workspace members and path dependencies appear. When `true`, all transitive
4554
/// dependencies with cached source are included.
55+
///
56+
/// Returns both the discovered crates and the resolved workspace root path.
4657
pub fn discover_crates(
4758
workspace_root: &Path,
4859
include_deps: bool,
4960
spawn_cap: &impl capsec_core::has::Has<capsec_core::permission::Spawn>,
5061
_fs_cap: &impl capsec_core::has::Has<capsec_core::permission::FsRead>,
51-
) -> Result<Vec<CrateInfo>, String> {
62+
) -> Result<DiscoveryResult, String> {
5263
// Use --no-deps by default for speed (avoids resolving 300+ transitive deps).
5364
// Drop it when --include-deps is set so path dependencies and registry crates appear.
5465
let mut args = vec!["metadata", "--format-version=1"];
@@ -70,6 +81,8 @@ pub fn discover_crates(
7081
let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
7182
.map_err(|e| format!("Failed to parse cargo metadata: {e}"))?;
7283

84+
let resolved_root = PathBuf::from(&metadata.workspace_root);
85+
7386
let mut crates = Vec::new();
7487

7588
for package in &metadata.packages {
@@ -90,7 +103,10 @@ pub fn discover_crates(
90103
}
91104
}
92105

93-
Ok(crates)
106+
Ok(DiscoveryResult {
107+
crates,
108+
workspace_root: resolved_root,
109+
})
94110
}
95111

96112
/// Recursively discovers all `.rs` source files in a directory.

0 commit comments

Comments
 (0)