Skip to content

Commit 3527988

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 7935204 commit 3527988

1 file changed

Lines changed: 80 additions & 2 deletions

File tree

src/assail/analyzer.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4285,15 +4285,22 @@ impl Analyzer {
42854285
.unwrap_or(false);
42864286

42874287
if !has_narhash && !has_rev_pin && !has_lockfile {
4288+
// The standard remediation is `nix flake update`, which
4289+
// generates a sibling flake.lock that pins every transitive
4290+
// input by narHash. Because the fix is trivial and
4291+
// mechanical, downgrade this finding to Low — it is a real
4292+
// supply-chain concern but not in the same class as e.g. an
4293+
// unsigned binary download or tamperable URL fetch.
42884294
weak_points.push(WeakPoint {
42894295
file: None,
42904296
line: None,
42914297
category: WeakPointCategory::SupplyChain,
42924298
location: Some(file_path.to_string()),
4293-
severity: Severity::High,
4299+
severity: Severity::Low,
42944300
description: format!(
42954301
"flake.nix declares inputs without narHash, rev pinning, \
4296-
or sibling flake.lock — dependency revision is unpinned in {}",
4302+
or sibling flake.lock — dependency revision is unpinned in {}. \
4303+
Suggested fix: run `nix flake update` to generate flake.lock.",
42974304
file_path
42984305
),
42994306
recommended_attack: vec![],
@@ -7763,4 +7770,75 @@ pub fn safe_get_x() -> Option<String> {
77637770
"unsafe fn / unsafe extern must not count toward the unsafe-block tally"
77647771
);
77657772
}
7773+
7774+
// ---------------------------------------------------------------
7775+
// flake.nix SupplyChain severity (downgrade to Low when fix is
7776+
// trivially mechanical — generate flake.lock).
7777+
// ---------------------------------------------------------------
7778+
7779+
fn flake_findings(content: &str, file_path: &str) -> Vec<WeakPoint> {
7780+
let analyzer = Analyzer::new(std::path::Path::new(".")).expect("analyzer construction");
7781+
let mut stats = ProgramStatistics::default();
7782+
let mut wp = Vec::new();
7783+
analyzer
7784+
.analyze_config(content, &mut stats, &mut wp, file_path)
7785+
.expect("analyze_config");
7786+
wp.into_iter()
7787+
.filter(|w| matches!(w.category, WeakPointCategory::SupplyChain))
7788+
.collect()
7789+
}
7790+
7791+
#[test]
7792+
fn flake_without_lock_is_low_severity() {
7793+
let src = r#"{
7794+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
7795+
outputs = { self, nixpkgs }: { };
7796+
}"#;
7797+
// Use a path that does NOT have a sibling flake.lock in the working dir.
7798+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7799+
assert_eq!(findings.len(), 1, "unpinned flake.nix must produce one finding");
7800+
assert!(
7801+
matches!(findings[0].severity, Severity::Low),
7802+
"missing flake.lock alone is mechanically fixable — must be Low severity, got {:?}",
7803+
findings[0].severity
7804+
);
7805+
assert!(
7806+
findings[0].description.contains("nix flake update"),
7807+
"description must point at the fix command"
7808+
);
7809+
}
7810+
7811+
#[test]
7812+
fn flake_with_narhash_has_no_finding() {
7813+
let src = r#"{
7814+
inputs.nixpkgs = {
7815+
url = "github:NixOS/nixpkgs/nixos-unstable";
7816+
narHash = "sha256-...";
7817+
};
7818+
outputs = { self, nixpkgs }: { };
7819+
}"#;
7820+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7821+
assert_eq!(
7822+
findings.len(),
7823+
0,
7824+
"flake.nix with inline narHash must NOT produce a SupplyChain finding"
7825+
);
7826+
}
7827+
7828+
#[test]
7829+
fn flake_with_rev_pins_has_no_finding() {
7830+
let src = r#"{
7831+
inputs.nixpkgs = {
7832+
url = "github:NixOS/nixpkgs/nixos-unstable";
7833+
rev = "abc123def456abc123def456abc123def456abcd";
7834+
};
7835+
outputs = { self, nixpkgs }: { };
7836+
}"#;
7837+
let findings = flake_findings(src, "/nonexistent/dir/flake.nix");
7838+
assert_eq!(
7839+
findings.len(),
7840+
0,
7841+
"flake.nix with rev pinning must NOT produce a SupplyChain finding"
7842+
);
7843+
}
77667844
}

0 commit comments

Comments
 (0)