Skip to content

Commit 5266c4b

Browse files
feat(assail): downgrade flake.lock-only SupplyChain to Severity::Low (mechanical fix) (#73)
## Summary When \`flake.nix\` declares inputs without inline \`narHash\`, without \`rev\` pinning, and without a sibling \`flake.lock\`, the standard remediation is a single \`nix flake update\` invocation that generates the lockfile with narHash for every transitive input. The fix is mechanical, so the finding does not belong in the same severity tier as e.g. an unsigned binary fetch or a tamperable URL. Downgrade from \`Severity::High\` to \`Severity::Low\` and embed the fix command directly in the description. The detector still triggers — the finding is real and worth surfacing — but it no longer dominates High-severity noise in scan reports. ## Motivation Across the 51 Track C panic-attack issues, missing \`flake.lock\` accounts for ~45 findings (e.g. \`julia-ecosystem#6\` carries 45 of these in the vendored monorepo). All are mechanically closable. Estate context: the Nix-mirror campaign closure (\`standards#149\`, \`hypatia#289\`) confirmed \`flake.lock\` generation is the canonical fix. ## Changes - \`src/assail/analyzer.rs\` (\`analyze_config\`): switch \`Severity::High\` → \`Severity::Low\` for the unpinned-flake case; append the suggested fix command to the description. - 3 regression tests: - \`flake_without_lock_is_low_severity\` — unpinned flake produces Low + suggestion - \`flake_with_narhash_has_no_finding\` — inline narHash suppresses - \`flake_with_rev_pins_has_no_finding\` — rev pinning suppresses ## Test plan - [x] \`cargo test --lib flake_\` — all 3 new tests pass locally - [ ] CI green 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b2e58a9 commit 5266c4b

1 file changed

Lines changed: 68 additions & 2 deletions

File tree

src/assail/analyzer.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4293,15 +4293,22 @@ impl Analyzer {
42934293
.unwrap_or(false);
42944294

42954295
if !has_narhash && !has_rev_pin && !has_lockfile {
4296+
// The standard remediation is `nix flake update`, which
4297+
// generates a sibling flake.lock that pins every transitive
4298+
// input by narHash. Because the fix is trivial and
4299+
// mechanical, downgrade this finding to Low — it is a real
4300+
// supply-chain concern but not in the same class as e.g. an
4301+
// unsigned binary download or tamperable URL fetch.
42964302
weak_points.push(WeakPoint {
42974303
file: None,
42984304
line: None,
42994305
category: WeakPointCategory::SupplyChain,
43004306
location: Some(file_path.to_string()),
4301-
severity: Severity::High,
4307+
severity: Severity::Low,
43024308
description: format!(
43034309
"flake.nix declares inputs without narHash, rev pinning, \
4304-
or sibling flake.lock — dependency revision is unpinned in {}",
4310+
or sibling flake.lock — dependency revision is unpinned in {}. \
4311+
Suggested fix: run `nix flake update` to generate flake.lock.",
43054312
file_path
43064313
),
43074314
recommended_attack: vec![],
@@ -7787,6 +7794,11 @@ pub fn safe_get_x() -> Option<String> {
77877794
// ---------------------------------------------------------------
77887795

77897796
fn count_julia_dce(content: &str, file_path: &str) -> usize {
7797+
// flake.nix SupplyChain severity (downgrade to Low when fix is
7798+
// trivially mechanical — generate flake.lock).
7799+
// ---------------------------------------------------------------
7800+
7801+
fn flake_findings(content: &str, file_path: &str) -> Vec<WeakPoint> {
77907802
let analyzer = Analyzer::new(std::path::Path::new(".")).expect("analyzer construction");
77917803
let mut stats = ProgramStatistics::default();
77927804
let mut wp = Vec::new();
@@ -7805,6 +7817,47 @@ pub fn safe_get_x() -> Option<String> {
78057817
count_julia_dce(src, "FooExt.jl"),
78067818
0,
78077819
"*Ext.jl files use eval/Meta.parse idiomatically — must be exempt"
7820+
.analyze_config(content, &mut stats, &mut wp, file_path)
7821+
.expect("analyze_config");
7822+
wp.into_iter()
7823+
.filter(|w| matches!(w.category, WeakPointCategory::SupplyChain))
7824+
.collect()
7825+
}
7826+
7827+
#[test]
7828+
fn flake_without_lock_is_low_severity() {
7829+
let src = r#"{
7830+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
7831+
outputs = { self, nixpkgs }: { };
7832+
}"#;
7833+
// Use a path that does NOT have a sibling flake.lock in the working dir.
7834+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7835+
assert_eq!(findings.len(), 1, "unpinned flake.nix must produce one finding");
7836+
assert!(
7837+
matches!(findings[0].severity, Severity::Low),
7838+
"missing flake.lock alone is mechanically fixable — must be Low severity, got {:?}",
7839+
findings[0].severity
7840+
);
7841+
assert!(
7842+
findings[0].description.contains("nix flake update"),
7843+
"description must point at the fix command"
7844+
);
7845+
}
7846+
7847+
#[test]
7848+
fn flake_with_narhash_has_no_finding() {
7849+
let src = r#"{
7850+
inputs.nixpkgs = {
7851+
url = "github:NixOS/nixpkgs/nixos-unstable";
7852+
narHash = "sha256-...";
7853+
};
7854+
outputs = { self, nixpkgs }: { };
7855+
}"#;
7856+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7857+
assert_eq!(
7858+
findings.len(),
7859+
0,
7860+
"flake.nix with inline narHash must NOT produce a SupplyChain finding"
78087861
);
78097862
}
78107863

@@ -7891,6 +7944,19 @@ pub fn safe_get_x() -> Option<String> {
78917944
.iter()
78927945
.any(|p| p.to_string_lossy().contains("rescript-ecosystem")),
78937946
"rescript-ecosystem vendored snapshot must be skipped"
7947+
fn flake_with_rev_pins_has_no_finding() {
7948+
let src = r#"{
7949+
inputs.nixpkgs = {
7950+
url = "github:NixOS/nixpkgs/nixos-unstable";
7951+
rev = "abc123def456abc123def456abc123def456abcd";
7952+
};
7953+
outputs = { self, nixpkgs }: { };
7954+
}"#;
7955+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7956+
assert_eq!(
7957+
findings.len(),
7958+
0,
7959+
"flake.nix with rev pinning must NOT produce a SupplyChain finding"
78947960
);
78957961
}
78967962
}

0 commit comments

Comments
 (0)