From da3fbb00bde551bf28c50b36d8b9d220df0619e3 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:20:49 -0600 Subject: [PATCH 1/4] feat(cli): add security field to mise tool --json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add security information to `mise tool --json` output showing aqua registry security metadata. The security field is an array of enabled security features with relevant metadata: - checksum (with algorithm) - github_attestations (with signer_workflow) - slsa - cosign - minisign (with public_key) For non-aqua backends, returns an empty array. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- e2e/cli/test_tool | 12 ++++++++++ src/backend/aqua.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++ src/backend/mod.rs | 23 +++++++++++++++++++ src/cli/tool.rs | 33 +++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/e2e/cli/test_tool b/e2e/cli/test_tool index b43029fe21..036829b419 100644 --- a/e2e/cli/test_tool +++ b/e2e/cli/test_tool @@ -18,3 +18,15 @@ assert_fail "mise tool INVALID_TOOL_NAME" mise use tiny@1.0.0 mise use tiny@1.1.0 assert "mise tool tiny --installed" "1.0.0 1.1.0" + +# Test security field exists in JSON output for aqua tool +mise tool aqua:cli/cli --json | jq -e '.security' || fail "security field not found in JSON" + +# Test security field is an array +mise tool aqua:cli/cli --json | jq -e '.security | type == "array"' || fail "security is not an array" + +# Test non-aqua tool has empty security array +mise tool node --json | jq -e '.security == []' || fail "non-aqua tool should have empty security" + +# Test that security features have type field when present +mise tool aqua:cli/cli --json | jq -e '.security | if length > 0 then .[0].type else true end' || fail "security feature missing type field" diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 0b95cecde7..eeefeffd39 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -54,6 +54,62 @@ impl Backend for AquaBackend { .and_then(|p| p.description.clone()) } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + let pkg = match AQUA_REGISTRY.package(&self.ba.tool_name).await { + Ok(pkg) => pkg, + Err(_) => return vec![], + }; + + let mut features = vec![]; + + // Checksum + if let Some(checksum) = &pkg.checksum { + if checksum.enabled() { + features.push(SecurityFeature::Checksum { + algorithm: checksum.algorithm.as_ref().map(|a| a.to_string()), + }); + } + } + + // GitHub Attestations + if let Some(attestations) = &pkg.github_artifact_attestations { + if attestations.enabled.unwrap_or(false) { + features.push(SecurityFeature::GithubAttestations { + signer_workflow: attestations.signer_workflow.clone(), + }); + } + } + + // SLSA + if let Some(slsa) = &pkg.slsa_provenance { + if slsa.enabled.unwrap_or(false) { + features.push(SecurityFeature::Slsa); + } + } + + // Cosign (nested in checksum) + if let Some(checksum) = &pkg.checksum { + if let Some(cosign) = &checksum.cosign { + if cosign.enabled.unwrap_or(false) { + features.push(SecurityFeature::Cosign); + } + } + } + + // Minisign + if let Some(minisign) = &pkg.minisign { + if minisign.enabled.unwrap_or(false) { + features.push(SecurityFeature::Minisign { + public_key: minisign.public_key.clone(), + }); + } + } + + features + } + fn ba(&self) -> &Arc { &self.ba } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 3cd2a3ef79..fd0e4cb196 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -120,6 +120,26 @@ impl VersionInfo { } } +/// Security feature information for a tool +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum SecurityFeature { + Checksum { + #[serde(skip_serializing_if = "Option::is_none")] + algorithm: Option, + }, + GithubAttestations { + #[serde(skip_serializing_if = "Option::is_none")] + signer_workflow: Option, + }, + Slsa, + Cosign, + Minisign { + #[serde(skip_serializing_if = "Option::is_none")] + public_key: Option, + }, +} + static TOOLS: Mutex>> = Mutex::new(None); pub async fn load_tools() -> Result> { @@ -295,6 +315,9 @@ pub trait Backend: Debug + Send + Sync { async fn description(&self) -> Option { None } + async fn security_info(&self) -> Vec { + vec![] + } fn get_plugin_type(&self) -> Option { None } diff --git a/src/cli/tool.rs b/src/cli/tool.rs index 625487d321..c8cc2f6d22 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -1,3 +1,4 @@ +use crate::backend::SecurityFeature; use crate::ui::style; use eyre::Result; use itertools::Itertools; @@ -75,10 +76,10 @@ impl Tool { } }; - let description = if let Some(backend) = backend { - backend.description().await + let (description, security) = if let Some(backend) = &backend { + (backend.description().await, backend.security_info().await) } else { - None + (None, vec![]) }; let info = ToolInfo { backend: ba.full(), @@ -105,6 +106,7 @@ impl Tool { }), config_source: tvl.map(|tvl| tvl.source.clone()), tool_options: ba.opts(), + security, }; if self.json { @@ -242,6 +244,30 @@ impl Tool { .join(","), )); } + if info.security.is_empty() { + table.push(("Security:", "[none]".to_string())); + } else { + let security_str = info + .security + .iter() + .map(|f| match f { + SecurityFeature::Checksum { algorithm } => { + if let Some(alg) = algorithm { + format!("checksum ({})", alg) + } else { + "checksum".to_string() + } + } + SecurityFeature::GithubAttestations { .. } => { + "github_attestations".to_string() + } + SecurityFeature::Slsa => "slsa".to_string(), + SecurityFeature::Cosign => "cosign".to_string(), + SecurityFeature::Minisign { .. } => "minisign".to_string(), + }) + .join(", "); + table.push(("Security:", security_str)); + } let mut table = tabled::Table::new(table); table::default_style(&mut table, true); miseprintln!("{table}"); @@ -260,6 +286,7 @@ struct ToolInfo { active_versions: Option>, config_source: Option, tool_options: ToolVersionOptions, + security: Vec, } static AFTER_LONG_HELP: &str = color_print::cstr!( From adcee7bceb1708e67fbcafa64b0c467b5f183fee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 03:29:19 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- src/backend/aqua.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index eeefeffd39..0cf5c792a8 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -65,47 +65,41 @@ impl Backend for AquaBackend { let mut features = vec![]; // Checksum - if let Some(checksum) = &pkg.checksum { - if checksum.enabled() { + if let Some(checksum) = &pkg.checksum + && checksum.enabled() { features.push(SecurityFeature::Checksum { algorithm: checksum.algorithm.as_ref().map(|a| a.to_string()), }); } - } // GitHub Attestations - if let Some(attestations) = &pkg.github_artifact_attestations { - if attestations.enabled.unwrap_or(false) { + if let Some(attestations) = &pkg.github_artifact_attestations + && attestations.enabled.unwrap_or(false) { features.push(SecurityFeature::GithubAttestations { signer_workflow: attestations.signer_workflow.clone(), }); } - } // SLSA - if let Some(slsa) = &pkg.slsa_provenance { - if slsa.enabled.unwrap_or(false) { + if let Some(slsa) = &pkg.slsa_provenance + && slsa.enabled.unwrap_or(false) { features.push(SecurityFeature::Slsa); } - } // Cosign (nested in checksum) - if let Some(checksum) = &pkg.checksum { - if let Some(cosign) = &checksum.cosign { - if cosign.enabled.unwrap_or(false) { + if let Some(checksum) = &pkg.checksum + && let Some(cosign) = &checksum.cosign + && cosign.enabled.unwrap_or(false) { features.push(SecurityFeature::Cosign); } - } - } // Minisign - if let Some(minisign) = &pkg.minisign { - if minisign.enabled.unwrap_or(false) { + if let Some(minisign) = &pkg.minisign + && minisign.enabled.unwrap_or(false) { features.push(SecurityFeature::Minisign { public_key: minisign.public_key.clone(), }); } - } features } From b42dea83d8e2ccf3b49109be0a4d122335e576e8 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:30:23 -0600 Subject: [PATCH 3/4] feat(cli): add security_info() to core plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend security field in `mise tool --json` to core plugins: - node: checksum (sha256) + GPG verification - zig: checksum (sha256) + minisign (with public key) - swift: checksum (sha256) + GPG verification (Linux only) - go, ruby, deno, bun: checksum (sha256) Also adds Gpg variant to SecurityFeature enum. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- e2e/cli/test_tool | 16 +++++++++++++--- src/backend/mod.rs | 1 + src/cli/tool.rs | 1 + src/plugins/core/bun.rs | 8 ++++++++ src/plugins/core/deno.rs | 8 ++++++++ src/plugins/core/go.rs | 9 +++++++++ src/plugins/core/node.rs | 15 +++++++++++++++ src/plugins/core/ruby.rs | 9 +++++++++ src/plugins/core/swift.rs | 15 +++++++++++++++ src/plugins/core/zig.rs | 13 +++++++++++++ 10 files changed, 92 insertions(+), 3 deletions(-) diff --git a/e2e/cli/test_tool b/e2e/cli/test_tool index 036829b419..9ceb2ac42c 100644 --- a/e2e/cli/test_tool +++ b/e2e/cli/test_tool @@ -25,8 +25,18 @@ mise tool aqua:cli/cli --json | jq -e '.security' || fail "security field not fo # Test security field is an array mise tool aqua:cli/cli --json | jq -e '.security | type == "array"' || fail "security is not an array" -# Test non-aqua tool has empty security array -mise tool node --json | jq -e '.security == []' || fail "non-aqua tool should have empty security" - # Test that security features have type field when present mise tool aqua:cli/cli --json | jq -e '.security | if length > 0 then .[0].type else true end' || fail "security feature missing type field" + +# Test core plugin security features +# Node has checksum + gpg +mise tool node --json | jq -e '.security | length >= 1' || fail "node should have security features" +mise tool node --json | jq -e '.security[] | select(.type == "checksum")' || fail "node should have checksum" + +# Go has checksum +mise tool go --json | jq -e '.security[] | select(.type == "checksum")' || fail "go should have checksum" + +# Zig has checksum + minisign +mise tool zig --json | jq -e '.security[] | select(.type == "checksum")' || fail "zig should have checksum" +mise tool zig --json | jq -e '.security[] | select(.type == "minisign")' || fail "zig should have minisign" +mise tool zig --json | jq -e '.security[] | select(.type == "minisign") | .public_key' || fail "zig minisign should have public_key" diff --git a/src/backend/mod.rs b/src/backend/mod.rs index fd0e4cb196..b190d6e8aa 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -138,6 +138,7 @@ pub enum SecurityFeature { #[serde(skip_serializing_if = "Option::is_none")] public_key: Option, }, + Gpg, } static TOOLS: Mutex>> = Mutex::new(None); diff --git a/src/cli/tool.rs b/src/cli/tool.rs index c8cc2f6d22..db5d23cd52 100644 --- a/src/cli/tool.rs +++ b/src/cli/tool.rs @@ -264,6 +264,7 @@ impl Tool { SecurityFeature::Slsa => "slsa".to_string(), SecurityFeature::Cosign => "cosign".to_string(), SecurityFeature::Minisign { .. } => "minisign".to_string(), + SecurityFeature::Gpg => "gpg".to_string(), }) .join(", "); table.push(("Security:", security_str)); diff --git a/src/plugins/core/bun.rs b/src/plugins/core/bun.rs index 032eb8f941..7a9ccf4393 100644 --- a/src/plugins/core/bun.rs +++ b/src/plugins/core/bun.rs @@ -96,6 +96,14 @@ impl Backend for BunPlugin { &self.ba } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }] + } + /// Override get_platform_key to include bun's compile-time variant (baseline, musl, etc.) /// This ensures lockfile lookups use the correct platform key that matches the variant fn get_platform_key(&self) -> String { diff --git a/src/plugins/core/deno.rs b/src/plugins/core/deno.rs index f85698bff5..a4acbf6574 100644 --- a/src/plugins/core/deno.rs +++ b/src/plugins/core/deno.rs @@ -94,6 +94,14 @@ impl Backend for DenoPlugin { &self.ba } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }] + } + async fn _list_remote_versions(&self, _config: &Arc) -> Result> { let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json").await?; let versions = versions diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index 2968decf25..acce4ac2c6 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -195,6 +195,15 @@ impl Backend for GoPlugin { fn ba(&self) -> &Arc { &self.ba } + + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }] + } + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { plugins::core::run_fetch_task_with_timeout(move || { let output = cmd!( diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 30b21dbbb8..8c36f0b2e1 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -404,6 +404,21 @@ impl Backend for NodePlugin { &self.ba } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + let mut features = vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }]; + + // GPG verification is available for Node.js v20+ when gpg is installed + if Settings::get().node.gpg_verify != Some(false) { + features.push(SecurityFeature::Gpg); + } + + features + } + async fn _list_remote_versions_with_info( &self, _config: &Arc, diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index 58e186f284..e1032ed61e 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -592,6 +592,15 @@ impl Backend for RubyPlugin { fn ba(&self) -> &Arc { &self.ba } + + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }] + } + async fn _list_remote_versions_with_info( &self, _config: &Arc, diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index f0aafb918b..a6d2778010 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -159,6 +159,21 @@ impl Backend for SwiftPlugin { &self.ba } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + let mut features = vec![SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }]; + + // GPG verification is available on Linux when gpg is installed + if cfg!(target_os = "linux") && Settings::get().swift.gpg_verify != Some(false) { + features.push(SecurityFeature::Gpg); + } + + features + } + async fn _list_remote_versions_with_info( &self, _config: &Arc, diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index 89b65577d4..d15878a978 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -242,6 +242,19 @@ impl Backend for ZigPlugin { &self.ba } + async fn security_info(&self) -> Vec { + use crate::backend::SecurityFeature; + + vec![ + SecurityFeature::Checksum { + algorithm: Some("sha256".to_string()), + }, + SecurityFeature::Minisign { + public_key: Some(ZIG_MINISIGN_KEY.to_string()), + }, + ] + } + async fn _list_remote_versions_with_info( &self, _config: &Arc, From 7d4169dc7f6536197b0ca232cedaa406da216db8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 03:53:02 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- src/backend/aqua.rs | 47 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 0cf5c792a8..dc0bd46155 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -66,40 +66,45 @@ impl Backend for AquaBackend { // Checksum if let Some(checksum) = &pkg.checksum - && checksum.enabled() { - features.push(SecurityFeature::Checksum { - algorithm: checksum.algorithm.as_ref().map(|a| a.to_string()), - }); - } + && checksum.enabled() + { + features.push(SecurityFeature::Checksum { + algorithm: checksum.algorithm.as_ref().map(|a| a.to_string()), + }); + } // GitHub Attestations if let Some(attestations) = &pkg.github_artifact_attestations - && attestations.enabled.unwrap_or(false) { - features.push(SecurityFeature::GithubAttestations { - signer_workflow: attestations.signer_workflow.clone(), - }); - } + && attestations.enabled.unwrap_or(false) + { + features.push(SecurityFeature::GithubAttestations { + signer_workflow: attestations.signer_workflow.clone(), + }); + } // SLSA if let Some(slsa) = &pkg.slsa_provenance - && slsa.enabled.unwrap_or(false) { - features.push(SecurityFeature::Slsa); - } + && slsa.enabled.unwrap_or(false) + { + features.push(SecurityFeature::Slsa); + } // Cosign (nested in checksum) if let Some(checksum) = &pkg.checksum && let Some(cosign) = &checksum.cosign - && cosign.enabled.unwrap_or(false) { - features.push(SecurityFeature::Cosign); - } + && cosign.enabled.unwrap_or(false) + { + features.push(SecurityFeature::Cosign); + } // Minisign if let Some(minisign) = &pkg.minisign - && minisign.enabled.unwrap_or(false) { - features.push(SecurityFeature::Minisign { - public_key: minisign.public_key.clone(), - }); - } + && minisign.enabled.unwrap_or(false) + { + features.push(SecurityFeature::Minisign { + public_key: minisign.public_key.clone(), + }); + } features }