Skip to content

Commit daeb0a7

Browse files
committed
feat: add cross-crate dependency audit with FFI propagation, workspace-to-workspace findings, and export map caching
1 parent 4b93e0d commit daeb0a7

2 files changed

Lines changed: 75 additions & 4 deletions

File tree

crates/cargo-capsec/src/export_map.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,69 @@ pub fn build_export_map(
124124

125125
CrateExportMap {
126126
crate_name: crate_name.to_string(),
127+
127128
crate_version: crate_version.to_string(),
128129
exports,
129130
}
130131
}
131132

133+
/// Adds extern function declarations from parsed files to an export map.
134+
///
135+
/// When a crate like `libgit2-sys` declares `extern "C" { fn git_repository_open(...); }`,
136+
/// this creates an FFI export entry for `git_repository_open` so that other crates
137+
/// calling `libgit2_sys::git_repository_open()` get a cross-crate FFI finding.
138+
///
139+
/// This is necessary because extern block findings have `function: "extern \"C\""` which
140+
/// produces useless export map keys. The individual function names need to be exported.
141+
pub fn add_extern_exports(
142+
export_map: &mut CrateExportMap,
143+
parsed_files: &[crate::parser::ParsedFile],
144+
src_dir: &Path,
145+
) {
146+
let crate_name = &export_map.crate_name;
147+
148+
for file in parsed_files {
149+
// Skip build.rs extern blocks (compile-time only)
150+
if file.path.ends_with("build.rs") {
151+
continue;
152+
}
153+
154+
for ext in &file.extern_blocks {
155+
let module_path = file_to_module_path(&file.path, src_dir);
156+
157+
for fn_name in &ext.functions {
158+
let auth = ExportedAuthority {
159+
category: crate::authorities::Category::Ffi,
160+
risk: crate::authorities::Risk::High,
161+
leaf_call: format!("extern {fn_name}"),
162+
is_transitive: false,
163+
};
164+
165+
// Full path: crate::module::fn_name
166+
let mut full_path = vec![crate_name.clone()];
167+
full_path.extend(module_path.clone());
168+
full_path.push(fn_name.clone());
169+
let key = full_path.join("::");
170+
export_map
171+
.exports
172+
.entry(key.clone())
173+
.or_default()
174+
.push(auth.clone());
175+
176+
// Short form: crate::fn_name (for crate-scoped matching)
177+
let short_key = format!("{crate_name}::{fn_name}");
178+
if short_key != key {
179+
export_map
180+
.exports
181+
.entry(short_key)
182+
.or_default()
183+
.push(auth);
184+
}
185+
}
186+
}
187+
}
188+
}
189+
132190
/// Cached export map format for disk persistence.
133191
#[derive(Debug, Serialize, Deserialize)]
134192
pub struct CachedExportMap {
@@ -140,7 +198,8 @@ pub struct CachedExportMap {
140198
}
141199

142200
/// Current schema version for cached export maps.
143-
pub const EXPORT_MAP_SCHEMA_VERSION: u32 = 1;
201+
/// Bumped to 2: added extern function declaration exports.
202+
pub const EXPORT_MAP_SCHEMA_VERSION: u32 = 2;
144203

145204
/// Attempts to load a cached export map for a dependency crate.
146205
///

crates/cargo-capsec/src/main.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,33 @@ fn run_audit(args: AuditArgs) {
9898

9999
let source_files = discovery::discover_source_files(&krate.source_dir, &fs_read);
100100
let mut dep_findings = Vec::new();
101+
let mut parsed_files = Vec::new();
101102

102103
for file_path in source_files {
103104
match parser::parse_file(&file_path, &fs_read) {
104105
Ok(parsed) => {
105106
let findings =
106107
det.analyse(&parsed, &krate.name, &krate.version, &crate_deny);
107108
dep_findings.extend(findings);
109+
parsed_files.push(parsed);
108110
}
109111
Err(_e) => {
110112
// Silently skip unparseable files in deps
111113
}
112114
}
113115
}
114116

115-
let emap = export_map::build_export_map(
117+
let mut emap = export_map::build_export_map(
116118
&normalized_name,
117119
&krate.version,
118120
&dep_findings,
119121
&krate.source_dir,
120122
);
121123

124+
// Also export extern function declarations (e.g., libgit2-sys, sqlite3-sys)
125+
// so callers like git2 get cross-crate FFI findings.
126+
export_map::add_extern_exports(&mut emap, &parsed_files, &krate.source_dir);
127+
122128
// Cache for registry deps
123129
if krate.is_dependency {
124130
export_map::save_export_map_cache(&cache_dir, &emap, &fs_write);
@@ -170,26 +176,29 @@ fn run_audit(args: AuditArgs) {
170176

171177
let source_files = discovery::discover_source_files(&krate.source_dir, &fs_read);
172178
let mut dep_findings = Vec::new();
179+
let mut parsed_files = Vec::new();
173180

174181
for file_path in source_files {
175182
match parser::parse_file(&file_path, &fs_read) {
176183
Ok(parsed) => {
177184
let findings =
178185
det.analyse(&parsed, &krate.name, &krate.version, &crate_deny);
179186
dep_findings.extend(findings);
187+
parsed_files.push(parsed);
180188
}
181189
Err(e) => {
182190
eprintln!(" Warning: {e}");
183191
}
184192
}
185193
}
186194

187-
let emap = export_map::build_export_map(
195+
let mut emap = export_map::build_export_map(
188196
&normalized_name,
189197
&krate.version,
190198
&dep_findings,
191199
&krate.source_dir,
192200
);
201+
export_map::add_extern_exports(&mut emap, &parsed_files, &krate.source_dir);
193202

194203
if krate.is_dependency {
195204
export_map::save_export_map_cache(&cache_dir, &emap, &fs_write);
@@ -274,6 +283,7 @@ fn run_audit(args: AuditArgs) {
274283

275284
let source_files = discovery::discover_source_files(&krate.source_dir, &fs_read);
276285
let mut ws_crate_findings = Vec::new();
286+
let mut ws_parsed_files = Vec::new();
277287

278288
for file_path in source_files {
279289
if config::should_exclude(&file_path, &cfg.analysis.exclude) {
@@ -285,6 +295,7 @@ fn run_audit(args: AuditArgs) {
285295
let findings =
286296
det.analyse(&parsed, &krate.name, &krate.version, &crate_deny);
287297
ws_crate_findings.extend(findings);
298+
ws_parsed_files.push(parsed);
288299
}
289300
Err(e) => {
290301
eprintln!(" Warning: {e}");
@@ -294,12 +305,13 @@ fn run_audit(args: AuditArgs) {
294305

295306
// Build export map for this workspace crate (for downstream ws crates)
296307
let normalized_name = discovery::normalize_crate_name(&krate.name);
297-
let ws_emap = export_map::build_export_map(
308+
let mut ws_emap = export_map::build_export_map(
298309
&normalized_name,
299310
&krate.version,
300311
&ws_crate_findings,
301312
&krate.source_dir,
302313
);
314+
export_map::add_extern_exports(&mut ws_emap, &ws_parsed_files, &krate.source_dir);
303315
workspace_export_maps.push(ws_emap);
304316

305317
all_findings.extend(ws_crate_findings);

0 commit comments

Comments
 (0)