Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e/cli/test_tool
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,25 @@ 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"
Comment on lines +23 to +26

Copilot AI Dec 15, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool name 'aqua:cli/cli' is repeated three times. Consider storing it in a variable to improve maintainability and make it easier to change the test tool if needed.

Copilot uses AI. Check for mistakes.

# 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"

Copilot AI Dec 15, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool name 'aqua:cli/cli' is repeated three times. Consider storing it in a variable to improve maintainability and make it easier to change the test tool if needed.

Copilot uses AI. Check for mistakes.

# 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"
55 changes: 55 additions & 0 deletions src/backend/aqua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,61 @@ impl Backend for AquaBackend {
.and_then(|p| p.description.clone())
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
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
&& 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(),
});
}

// SLSA
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
&& let Some(cosign) = &checksum.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(),
});
}

features
}

fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}
Expand Down
24 changes: 24 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ 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<String>,
},
GithubAttestations {
#[serde(skip_serializing_if = "Option::is_none")]
signer_workflow: Option<String>,
},
Slsa,
Cosign,
Minisign {
#[serde(skip_serializing_if = "Option::is_none")]
public_key: Option<String>,
},
Gpg,
}

static TOOLS: Mutex<Option<Arc<BackendMap>>> = Mutex::new(None);

pub async fn load_tools() -> Result<Arc<BackendMap>> {
Expand Down Expand Up @@ -295,6 +316,9 @@ pub trait Backend: Debug + Send + Sync {
async fn description(&self) -> Option<String> {
None
}
async fn security_info(&self) -> Vec<SecurityFeature> {
vec![]
}
fn get_plugin_type(&self) -> Option<PluginType> {
None
}
Expand Down
34 changes: 31 additions & 3 deletions src/cli/tool.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::backend::SecurityFeature;
use crate::ui::style;
use eyre::Result;
use itertools::Itertools;
Expand Down Expand Up @@ -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(),
Expand All @@ -105,6 +106,7 @@ impl Tool {
}),
config_source: tvl.map(|tvl| tvl.source.clone()),
tool_options: ba.opts(),
security,
};

if self.json {
Expand Down Expand Up @@ -242,6 +244,31 @@ 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(),
SecurityFeature::Gpg => "gpg".to_string(),
})
.join(", ");
table.push(("Security:", security_str));
}
let mut table = tabled::Table::new(table);
table::default_style(&mut table, true);
miseprintln!("{table}");
Expand All @@ -260,6 +287,7 @@ struct ToolInfo {
active_versions: Option<Vec<String>>,
config_source: Option<ToolSource>,
tool_options: ToolVersionOptions,
security: Vec<SecurityFeature>,
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/core/bun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ impl Backend for BunPlugin {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
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 {
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/core/deno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ impl Backend for DenoPlugin {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
use crate::backend::SecurityFeature;

vec![SecurityFeature::Checksum {
algorithm: Some("sha256".to_string()),
}]
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<String>> {
let versions: DenoVersions = HTTP_FETCH.json("https://deno.com/versions.json").await?;
let versions = versions
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/core/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ impl Backend for GoPlugin {
fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
use crate::backend::SecurityFeature;

vec![SecurityFeature::Checksum {
algorithm: Some("sha256".to_string()),
}]
}

async fn _list_remote_versions(&self, _config: &Arc<Config>) -> eyre::Result<Vec<String>> {
plugins::core::run_fetch_task_with_timeout(move || {
let output = cmd!(
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/core/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ impl Backend for NodePlugin {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
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<Config>,
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/core/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,15 @@ impl Backend for RubyPlugin {
fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
use crate::backend::SecurityFeature;

vec![SecurityFeature::Checksum {
algorithm: Some("sha256".to_string()),
}]
}

async fn _list_remote_versions_with_info(
&self,
_config: &Arc<Config>,
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/core/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ impl Backend for SwiftPlugin {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
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<Config>,
Expand Down
13 changes: 13 additions & 0 deletions src/plugins/core/zig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,19 @@ impl Backend for ZigPlugin {
&self.ba
}

async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
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<Config>,
Expand Down
Loading