Skip to content

Commit a9664a1

Browse files
hyperpolymathclaude
andcommitted
feat(assail): downgrade flake.lock-only SupplyChain finding to Severity::Low
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. Because the fix is trivial and mechanical, this finding does not belong in the same severity tier as e.g. an unsigned binary fetch or a tamperable URL. Downgrade from High to Low and embed the fix command directly in the description. The detector still triggers — the finding is real and worth noting — but it no longer dominates noise tiers in scan reports. Estate context: the Nix-mirror campaign closure (standards#149, hypatia#289) confirmed flake.lock generation is the canonical mechanical fix. Regression tests verify (a) the Low severity, (b) the suggestion text, (c) that flakes with narHash or rev pinning remain finding-free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b2e58a9 commit a9664a1

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)