feat: harden download and install security
This commit is contained in:
parent
f8ffb95376
commit
af13e98eb3
33 changed files with 1517 additions and 46 deletions
|
|
@ -4,6 +4,8 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize)]
|
||||
pub struct CliConfig {
|
||||
#[serde(default)]
|
||||
pub allow_http: bool,
|
||||
#[serde(default)]
|
||||
pub search: SearchConfig,
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use std::env;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use aim_core::app::add::{
|
||||
AddPlan, InstalledApp, build_add_plan_with_reporter, install_app_with_reporter,
|
||||
resolve_requested_scope,
|
||||
AddPlan, AddSecurityPolicy, InstalledApp, build_add_plan_with_reporter_and_policy,
|
||||
install_app_with_reporter, resolve_requested_scope,
|
||||
};
|
||||
use aim_core::app::list::{ListRow, build_list_rows};
|
||||
use aim_core::app::progress::{
|
||||
|
|
@ -17,7 +17,7 @@ use aim_core::app::progress::{
|
|||
use aim_core::app::remove::{RemovalResult, remove_registered_app_with_reporter};
|
||||
use aim_core::app::search::build_search_results;
|
||||
use aim_core::app::show::{build_installed_show_results, build_show_result};
|
||||
use aim_core::app::update::{build_update_plan, execute_updates_with_reporter};
|
||||
use aim_core::app::update::{build_update_plan, execute_updates_with_reporter_and_policy};
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::search::{SearchQuery, SearchResults};
|
||||
use aim_core::domain::show::{InstalledShow, ShowResult};
|
||||
|
|
@ -38,6 +38,14 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
|
|||
pub fn dispatch_with_reporter(
|
||||
cli: Cli,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<DispatchResult, DispatchError> {
|
||||
dispatch_with_reporter_and_config(cli, &crate::config::CliConfig::default(), reporter)
|
||||
}
|
||||
|
||||
pub fn dispatch_with_reporter_and_config(
|
||||
cli: Cli,
|
||||
config: &crate::config::CliConfig,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<DispatchResult, DispatchError> {
|
||||
let registry_path = registry_path();
|
||||
let install_home = install_home(®istry_path);
|
||||
|
|
@ -86,7 +94,14 @@ pub fn dispatch_with_reporter(
|
|||
None => Ok(DispatchResult::ShowAll(build_installed_show_results(&apps))),
|
||||
},
|
||||
cli::args::Command::Update => {
|
||||
let updates = execute_updates_with_reporter(&apps, &install_home, reporter)?;
|
||||
let updates = execute_updates_with_reporter_and_policy(
|
||||
&apps,
|
||||
&install_home,
|
||||
reporter,
|
||||
AddSecurityPolicy {
|
||||
allow_http_user_sources: config.allow_http,
|
||||
},
|
||||
)?;
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::SaveRegistry,
|
||||
message: "saving registry".to_owned(),
|
||||
|
|
@ -109,7 +124,14 @@ pub fn dispatch_with_reporter(
|
|||
if let Some(query) = cli.query {
|
||||
let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root());
|
||||
let transport = aim_core::source::github::default_transport();
|
||||
let plan_result = build_add_plan_with_reporter(&query, transport.as_ref(), reporter);
|
||||
let plan_result = build_add_plan_with_reporter_and_policy(
|
||||
&query,
|
||||
transport.as_ref(),
|
||||
reporter,
|
||||
AddSecurityPolicy {
|
||||
allow_http_user_sources: config.allow_http,
|
||||
},
|
||||
);
|
||||
let mut plan = match plan_result {
|
||||
Ok(plan) => plan,
|
||||
Err(
|
||||
|
|
@ -209,6 +231,10 @@ impl std::fmt::Display for DispatchError {
|
|||
aim_core::app::add::BuildAddPlanError::Query(
|
||||
aim_core::app::query::ResolveQueryError::Unsupported,
|
||||
) => write!(f, "unsupported source query"),
|
||||
aim_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!(
|
||||
f,
|
||||
"insecure HTTP sources are disabled; set allow_http = true to permit them"
|
||||
),
|
||||
aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { source } => write!(
|
||||
f,
|
||||
"no installable artifact found for {} {}",
|
||||
|
|
@ -233,7 +259,7 @@ impl std::fmt::Display for DispatchError {
|
|||
write!(f, "no installable candidates found")
|
||||
}
|
||||
},
|
||||
Self::AddInstall(error) => write!(f, "install failed: {error:?}"),
|
||||
Self::AddInstall(error) => write!(f, "install failed: {}", render_install_error(error)),
|
||||
Self::Prompt(error) => write!(f, "prompt failed: {error:?}"),
|
||||
Self::RemovePlan(error) => write!(f, "remove failed: {error:?}"),
|
||||
Self::Registry(error) => write!(f, "registry failed: {error:?}"),
|
||||
|
|
@ -250,6 +276,10 @@ impl std::fmt::Display for DispatchError {
|
|||
aim_core::domain::show::ShowResultError::UnsupportedQuery => {
|
||||
write!(f, "unsupported source query")
|
||||
}
|
||||
aim_core::domain::show::ShowResultError::InsecureHttpSource => write!(
|
||||
f,
|
||||
"insecure HTTP sources are disabled; set allow_http = true to permit them"
|
||||
),
|
||||
aim_core::domain::show::ShowResultError::NoInstallableArtifact { source } => {
|
||||
write!(
|
||||
f,
|
||||
|
|
@ -307,6 +337,17 @@ impl std::fmt::Display for DispatchError {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_install_error(error: &aim_core::app::add::InstallAppError) -> String {
|
||||
match error {
|
||||
aim_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"),
|
||||
aim_core::app::add::InstallAppError::Policy(error) => error.clone(),
|
||||
aim_core::app::add::InstallAppError::Download(error) => error.to_string(),
|
||||
aim_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(),
|
||||
aim_core::app::add::InstallAppError::HostProbe(error) => error.to_string(),
|
||||
aim_core::app::add::InstallAppError::Install(error) => error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
|
||||
fn from(value: aim_core::app::add::BuildAddPlanError) -> Self {
|
||||
Self::AddPlan(value)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fn main() {
|
|||
|
||||
let cli = aim_cli::parse();
|
||||
let mut reporter = aim_cli::ui::progress::TerminalProgressReporter::stderr();
|
||||
match aim_cli::dispatch_with_reporter(cli, &mut reporter) {
|
||||
match aim_cli::dispatch_with_reporter_and_config(cli, &config, &mut reporter) {
|
||||
Ok(result) => {
|
||||
let output = aim_cli::render_with_config(&result, &config);
|
||||
if !output.is_empty() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ fn missing_config_file_returns_defaults() {
|
|||
|
||||
assert_eq!(config, CliConfig::default());
|
||||
assert_eq!(config.search, SearchConfig::default());
|
||||
assert!(!config.allow_http);
|
||||
assert!(config.search.bottom_to_top);
|
||||
assert!(!config.search.skip_confirmation);
|
||||
assert_eq!(config.theme.accent, "#b388ff");
|
||||
|
|
@ -23,7 +24,7 @@ fn search_section_overrides_defaults() {
|
|||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
|
||||
"allow_http = true\n\n[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ fn search_section_overrides_defaults() {
|
|||
assert_eq!(
|
||||
config,
|
||||
CliConfig {
|
||||
allow_http: true,
|
||||
search: SearchConfig {
|
||||
bottom_to_top: false,
|
||||
skip_confirmation: true,
|
||||
|
|
|
|||
|
|
@ -339,6 +339,74 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() {
|
|||
assert!(contents.contains("canonical_locator = \"team-app\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_insecure_http_direct_urls_by_default() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("http://example.com/team-app.AppImage")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("insecure HTTP sources are disabled"));
|
||||
|
||||
assert!(!registry_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_allows_insecure_http_direct_urls_when_config_enables_it() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "allow_http = true\n").unwrap();
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("http://example.com/team-app.AppImage")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env("AIM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Installed"))
|
||||
.stdout(contains(
|
||||
"Source: direct-url http://example.com/team-app.AppImage",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let config_path = dir.path().join("config.toml");
|
||||
std::fs::write(&config_path, "allow_http = true\n").unwrap();
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2338455")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env("AIM_CONFIG_PATH", &config_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("insecure appimagehub download url"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_rejects_appimagehub_install_when_md5_does_not_match() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("appimagehub/2338455")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.env("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("weak provider checksum did not match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_sourceforge_release_folder_with_truthful_origin() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ fn install_summary_omits_completed_steps_recap() {
|
|||
version: "0.25.0".to_owned(),
|
||||
arch: Some("x86_64".to_owned()),
|
||||
trusted_checksum: None,
|
||||
weak_checksum_md5: None,
|
||||
selection_reason: "heuristic-match".to_owned(),
|
||||
},
|
||||
artifact_size_bytes: 173_015_040,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue