Skip to content

Commit 3edae66

Browse files
authored
feat(pq): dependency-level PQ scanning (closes #221) (#260)
* feat(pq): dependency-level PQ scanning for Cargo.lock and requirements.txt (closes #221) Scan manifest/lock files against curated crypto package databases to surface PQ-vulnerable dependencies that source-level rules can't see. - CargoLockPqCrypto: BFS transitive graph traversal with 6 tier-1 seeds (RSA, ECDSA, Ed25519, X25519 at 0.9 confidence) and 3 tier-2 seeds (ring, openssl-sys, aws-lc-rs at 0.6) - RequirementsTxtPqCrypto: direct lookup against 11 curated packages with per-package confidence (0.5-0.95), PEP 503 normalization - New dep_name field on Finding for manifest-level attribution - Replace is_pq_rule_id substring hack with explicit whitelist - Language::Manifest via bash-dummy tree-sitter pattern * fix(pq): address CI and review feedback for manifest scanning - Fix clippy collapsible_str_replace: use replace(['_', '.'], "-") - Fix rustfmt formatting for struct initializer arrays - Fix source.find() returning first occurrence for duplicate crate names by searching for unique name+version pair - Fix CRLF line ending drift in requirements.txt byte offset tracking - Normalize pip_map keys so future entries with mixed case won't break * fix(pq): prevent is_pq_rule_id false positive on go/insecure-tls-skip-verify id.contains("insecure-tls") matches non-PQ rules like go/insecure-tls-skip-verify. Use exact match for the one Dockerfile rule that belongs in the PQ set. * fix(pq): address manifest scanning correctness issues - Remove find_name_version_offset fallback that silently picked wrong package entry when name+version pair wasn't adjacent - Stop BFS at seed crates instead of traversing through their deps - Handle bare CR line endings in requirements.txt offset tracking - Use .first().unwrap() over index [0] for reached_seeds - Unify CrateEntry/PipEntry into SeedEntry, extract shared constants * test(pq): add unit tests for manifest scanning rules Cover CargoLockPqCrypto (BFS graph walk, seed detection, transitive deps, version-qualified strings, confidence tiebreaking, BFS stopping at seeds), RequirementsTxtPqCrypto (PEP 503 normalization, CRLF offsets, comments/options skipping, environment markers, extras), and find_name_version_offset (disambiguation, CRLF, missing entries). * test(pq): strengthen manifest test assertions - Redesign BFS-stops-at-seed test: use ring (0.6) → rsa (0.9) chain so confidence value distinguishes correct vs incorrect traversal - Assert column and end_column in CRLF offset test, not just line - Add test for name-exists-but-version-mismatches → returns None - Add multi-version diamond test (syn 1.x vs 2.x with version- qualified dep strings) * fix(pq): resolve version-qualified dep strings to exact package Version-qualified dep strings like "syn 2.0.0" were split on space but the version was discarded — edges fanned out to all indices with that crate name regardless of version. Add name_ver_to_index lookup so version-qualified strings resolve to exactly one package. Strengthen multi-version diamond test: syn 1.0 now depends on ring (0.6) while syn 2.0 depends on rsa (0.9), so incorrect edge resolution would produce a different confidence value. * test(pq): fix tautological multi-version diamond assertion Swap seeds: syn 1.0 → rsa (0.9), syn 2.0 → ring (0.6). Old buggy code (fan-out to both versions) would reach rsa and report 0.9. Correct code resolves to syn 2.0 only → ring → 0.6. The assertion now actually discriminates the bug from the fix. * fix(pq): use max_by instead of sort_by for highest-confidence seed Avoids mutating the vec just to pick one element.
1 parent 39127c1 commit 3edae66

28 files changed

Lines changed: 1132 additions & 4 deletions

Cargo.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ globset = "0.4"
3737
tempfile = "3"
3838
ratatui = "0.30"
3939
crossterm = "0.28"
40+
toml = "0.8"
4041

4142
[[bin]]
4243
name = "foxguard-mcp"

src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ fn validate_rules_path(rules: Option<&str>) -> Result<(), String> {
495495
fn is_pq_rule_id(id: &str) -> bool {
496496
id.contains("pq-vulnerable")
497497
|| id.contains("hardcoded-crypto-algorithm")
498-
|| (id.starts_with("config/") && id.contains("tls"))
498+
|| id == "config/dockerfile-insecure-tls-env"
499499
}
500500

501501
fn collect_changed_targets(path: &str, changed: bool) -> Result<Option<Vec<PathBuf>>, String> {

src/baseline.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ mod tests {
176176
tags: vec![],
177177
crypto_algorithm: None,
178178
cnsa2_deadline: None,
179+
dep_name: None,
179180
}
180181
}
181182

src/bin/gen_rules_ts.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const LANGUAGE_ORDER: &[Language] = &[
2929
Language::ApacheConf,
3030
Language::HAProxyConf,
3131
Language::Dockerfile,
32+
Language::Manifest,
3233
];
3334

3435
fn language_slug(language: Language) -> &'static str {
@@ -47,6 +48,7 @@ fn language_slug(language: Language) -> &'static str {
4748
Language::ApacheConf => "apacheconf",
4849
Language::HAProxyConf => "haproxyconf",
4950
Language::Dockerfile => "dockerfile",
51+
Language::Manifest => "manifest",
5052
}
5153
}
5254

@@ -66,6 +68,7 @@ fn language_display_name(language: Language) -> &'static str {
6668
Language::ApacheConf => "Apache",
6769
Language::HAProxyConf => "HAProxy",
6870
Language::Dockerfile => "Dockerfile",
71+
Language::Manifest => "Manifest",
6972
}
7073
}
7174

@@ -85,6 +88,7 @@ fn language_array_name(language: Language) -> &'static str {
8588
Language::ApacheConf => "apacheconfRules",
8689
Language::HAProxyConf => "haproxyconfRules",
8790
Language::Dockerfile => "dockerfileRules",
91+
Language::Manifest => "manifestRules",
8892
}
8993
}
9094

src/compliance.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ mod tests {
262262
tags: vec![],
263263
crypto_algorithm: None,
264264
cnsa2_deadline: deadline.map(String::from),
265+
dep_name: None,
265266
}
266267
}
267268

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ mod tests {
12381238
tags: vec![],
12391239
crypto_algorithm: None,
12401240
cnsa2_deadline: None,
1241+
dep_name: None,
12411242
};
12421243

12431244
let (config_path, added) =
@@ -1303,6 +1304,7 @@ mod tests {
13031304
tags: vec![],
13041305
crypto_algorithm: None,
13051306
cnsa2_deadline: None,
1307+
dep_name: None,
13061308
}
13071309
}
13081310

src/diff.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ mod tests {
289289
tags: vec![],
290290
crypto_algorithm: None,
291291
cnsa2_deadline: None,
292+
dep_name: None,
292293
}
293294
}
294295

src/engine/parser.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pub fn parse_file(source: &str, language: Language) -> Option<tree_sitter::Tree>
1818
Language::NginxConf
1919
| Language::ApacheConf
2020
| Language::HAProxyConf
21-
| Language::Dockerfile => tree_sitter_bash::LANGUAGE.into(),
21+
| Language::Dockerfile
22+
| Language::Manifest => tree_sitter_bash::LANGUAGE.into(),
2223
};
2324

2425
parser.set_language(&ts_language).ok()?;

src/engine/scanner.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ fn detect_config_language(path: &Path) -> Option<Language> {
116116
"nginx.conf" => return Some(Language::NginxConf),
117117
"httpd.conf" | "apache2.conf" => return Some(Language::ApacheConf),
118118
"haproxy.cfg" => return Some(Language::HAProxyConf),
119+
"Cargo.lock" | "requirements.txt" => return Some(Language::Manifest),
119120
_ => {}
120121
}
121122

@@ -492,7 +493,8 @@ fn comment_markers(language: Language) -> &'static [&'static str] {
492493
Language::NginxConf
493494
| Language::ApacheConf
494495
| Language::HAProxyConf
495-
| Language::Dockerfile => &["#"],
496+
| Language::Dockerfile
497+
| Language::Manifest => &["#"],
496498
}
497499
}
498500

0 commit comments

Comments
 (0)