217 lines
9.4 KiB
Rust
217 lines
9.4 KiB
Rust
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
|
// version: 6
|
|
|
|
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
|
|
|
fn app_root() -> std::path::PathBuf {
|
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
}
|
|
|
|
fn read_text(path: &std::path::Path) -> String {
|
|
let source = std::fs::read_to_string(path);
|
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
|
return match source {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => String::new(),
|
|
};
|
|
}
|
|
|
|
fn read_rust_source_tree(root: &std::path::Path) -> String {
|
|
let entries = std::fs::read_dir(root);
|
|
assert!(entries.is_ok(), "unable to enumerate {}", root.display());
|
|
let entries = match entries {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return String::new(),
|
|
};
|
|
let mut paths = std::vec::Vec::new();
|
|
for entry in entries {
|
|
let entry = match entry {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => continue,
|
|
};
|
|
let path = entry.path();
|
|
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
|
|
paths.push(path);
|
|
}
|
|
}
|
|
paths.sort();
|
|
let mut combined = String::new();
|
|
for path in paths {
|
|
combined.push_str(read_text(path.as_path()).as_str());
|
|
combined.push('\n');
|
|
}
|
|
return combined;
|
|
}
|
|
|
|
fn struct_field_lines(source: &str, name: &str) -> String {
|
|
let marker = std::format!("struct {name}");
|
|
let start = source.find(marker.as_str());
|
|
assert!(start.is_some(), "missing struct {name}");
|
|
let start = match start {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return String::new(),
|
|
};
|
|
let block = source[start..].split("}\n").next().unwrap_or_default();
|
|
return block.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
|
}
|
|
|
|
#[test]
|
|
fn backend_keeps_config_wallet_transport_and_logging_ownership_boundaries() {
|
|
let root = app_root();
|
|
let source = read_rust_source_tree(root.join("src").as_path());
|
|
for forbidden in [
|
|
"std::env::var(",
|
|
"std::env::var_os(",
|
|
"std::env::vars(",
|
|
"reqwest::",
|
|
"solana_keypair::",
|
|
"solana_pubkey::",
|
|
"solana_signer::",
|
|
"solana_signature::",
|
|
] {
|
|
assert!(!source.contains(forbidden), "Wallet Desk backend bypasses an owned boundary through {forbidden}");
|
|
}
|
|
let tracing_path = ["tracing", "::"].concat();
|
|
let tracing_use = std::format!("use {tracing_path}");
|
|
for line in source.lines() {
|
|
let trimmed = line.trim_start();
|
|
assert!(!trimmed.starts_with(tracing_use.as_str()), "Wallet Desk backend must not import tracing directly: {trimmed}");
|
|
assert!(!trimmed.starts_with(tracing_path.as_str()), "Wallet Desk backend must not emit through tracing directly: {trimmed}");
|
|
}
|
|
assert!(source.contains("ksp_config_lib::ConfigEnvironment::load"));
|
|
assert!(source.contains("ksp_wallet_lib::"));
|
|
assert!(source.contains("ksp_onchain_transport_lib::"));
|
|
assert!(source.contains("ksp_logging_lib::"));
|
|
}
|
|
|
|
#[test]
|
|
fn frontend_has_no_direct_filesystem_network_or_secret_persistence_surface() {
|
|
let root = app_root();
|
|
let package = read_text(root.join("package.json").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
|
|
let frontend_log = read_text(root.join("frontend/ts/frontend_log.ts").as_path());
|
|
let splash = read_text(root.join("frontend/ts/splash.ts").as_path());
|
|
let html = read_text(root.join("frontend/main.html").as_path());
|
|
let combined = std::format!("{main}\n{invoke}\n{frontend_log}\n{splash}\n{html}");
|
|
for forbidden in [
|
|
"@tauri-apps/plugin-dialog",
|
|
"@tauri-apps/plugin-fs",
|
|
"navigator.clipboard",
|
|
"localStorage",
|
|
"sessionStorage",
|
|
"window.alert",
|
|
"window.confirm",
|
|
"window.prompt",
|
|
"XMLHttpRequest",
|
|
"new WebSocket",
|
|
"EventSource",
|
|
"fetch(",
|
|
"KSP_SECRET_WALLET_PASS_",
|
|
"sourcePath",
|
|
"source_path",
|
|
"destinationPath",
|
|
"destination_path",
|
|
] {
|
|
assert!(!combined.contains(forbidden), "frontend contains forbidden direct surface {forbidden}");
|
|
}
|
|
assert!(!package.contains("@tauri-apps/plugin-dialog"));
|
|
assert!(!package.contains("@tauri-apps/plugin-fs"));
|
|
}
|
|
|
|
#[test]
|
|
fn response_dtos_keep_secret_key_material_out_of_ipc() {
|
|
let root = app_root();
|
|
let session = read_text(root.join("src/wallet_session.rs").as_path());
|
|
let balance = read_text(root.join("src/wallet_balance.rs").as_path());
|
|
let import = read_text(root.join("src/wallet_import.rs").as_path());
|
|
let export = read_text(root.join("src/wallet_export.rs").as_path());
|
|
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
|
let common = read_text(root.join("src/dto_common.rs").as_path());
|
|
for (source, name) in [
|
|
(session.as_str(), "WalletAuthorizedDto"),
|
|
(session.as_str(), "WalletSessionDto"),
|
|
(balance.as_str(), "WalletBalanceDto"),
|
|
(import.as_str(), "WalletTransferInspectionDto"),
|
|
(export.as_str(), "WalletExportResultDto"),
|
|
(security.as_str(), "WalletViewSecurityStatusDto"),
|
|
(common.as_str(), "RuntimeStatusDto"),
|
|
] {
|
|
let fields = struct_field_lines(source, name);
|
|
for forbidden in ["password", "secret_key", "private_key", "keypair_bytes", "ciphertext"] {
|
|
assert!(!fields.contains(forbidden), "response DTO {name} contains forbidden field material {forbidden}");
|
|
}
|
|
}
|
|
let inspection = struct_field_lines(import.as_str(), "WalletTransferInspectionDto");
|
|
let export_result = struct_field_lines(export.as_str(), "WalletExportResultDto");
|
|
assert!(!inspection.contains("path"));
|
|
assert!(!export_result.contains("path"));
|
|
assert!(!export_result.contains("content"));
|
|
assert!(!export_result.contains("bytes"));
|
|
}
|
|
|
|
#[test]
|
|
fn wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal() {
|
|
let root = app_root();
|
|
let manifest = read_text(root.join("Cargo.toml").as_path());
|
|
for line in manifest.lines() {
|
|
let trimmed = line.trim_start();
|
|
assert!(!trimmed.starts_with("solana-"), "Wallet Desk must consume Solana behavior through KSP crates: {trimmed}");
|
|
assert!(!trimmed.starts_with("reqwest "), "Wallet Desk must consume HTTP through Transport: {trimmed}");
|
|
assert!(!trimmed.starts_with("tracing "), "Wallet Desk must consume logging through the KSP facade/Tauri adapter: {trimmed}");
|
|
}
|
|
let capability_source = read_text(root.join("capabilities/default.json").as_path());
|
|
let capability = serde_json::from_str::<serde_json::Value>(capability_source.as_str());
|
|
assert!(capability.is_ok(), "Wallet Desk capability JSON should parse");
|
|
let capability = match capability {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let permissions = capability.get("permissions").and_then(serde_json::Value::as_array);
|
|
assert!(permissions.is_some());
|
|
let permissions = match permissions {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(permissions.len(), 2);
|
|
assert_eq!(permissions[0].as_str(), std::option::Option::Some("core:default"));
|
|
assert_eq!(permissions[1].as_str(), std::option::Option::Some("tracing:default"));
|
|
let tauri = read_text(root.join("tauri.conf.json").as_path());
|
|
assert!(tauri.contains("\"csp\": null"));
|
|
assert!(!capability_source.contains("dialog:"));
|
|
assert!(!capability_source.contains("fs:"));
|
|
}
|
|
|
|
#[test]
|
|
fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
|
let root = app_root();
|
|
let tauri_source = read_text(root.join("tauri.conf.json").as_path());
|
|
let tauri = serde_json::from_str::<serde_json::Value>(tauri_source.as_str());
|
|
assert!(tauri.is_ok(), "Wallet Desk Tauri config should parse");
|
|
let tauri = match tauri {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
|
assert!(resources.is_some(), "Wallet Desk package must declare Config resources");
|
|
let resources = match resources {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(resources.len(), 10);
|
|
for (source, destination) in resources {
|
|
let destination = destination.as_str();
|
|
assert!(destination.is_some(), "resource destination must be textual");
|
|
let destination = match destination {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
assert!(source.starts_with("../../config/"));
|
|
assert!(destination.starts_with("config/"));
|
|
for forbidden in [ksp_config_lib::DEFAULT_DOTENV_PATH, "wallets", "logs", "secret", "private"] {
|
|
assert!(!source.contains(forbidden), "mutable/secret runtime resource must not be bundled: {source}");
|
|
assert!(!destination.contains(forbidden), "mutable/secret runtime destination must not be bundled: {destination}");
|
|
}
|
|
}
|
|
}
|