281 lines
15 KiB
Rust
281 lines
15 KiB
Rust
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
|
|
// version: 12
|
|
|
|
//! Security boundary tests for the Store Desk scaffold.
|
|
|
|
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
|
|
return serde_json::from_str::<serde_json::Value>(source);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_capabilities_are_core_and_tracing_only() {
|
|
let parsed = parse_json(include_str!("../capabilities/default.json"));
|
|
assert!(parsed.is_ok(), "committed capability JSON must be valid");
|
|
let capability = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let permissions = capability["permissions"].as_array();
|
|
assert!(permissions.is_some(), "capability permissions must be an array");
|
|
let permissions = match permissions {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert_eq!(permissions.len(), 2);
|
|
assert_eq!(permissions[0], "core:default");
|
|
assert_eq!(permissions[1], "tracing:default");
|
|
}
|
|
|
|
#[test]
|
|
fn pre_002_frontend_avoids_network_browser_storage_and_blocking_dialogs() {
|
|
let frontend = [
|
|
include_str!("../frontend/ts/frontend_log.ts"),
|
|
include_str!("../frontend/ts/invoke.ts"),
|
|
include_str!("../frontend/ts/main.ts"),
|
|
include_str!("../frontend/ts/splash.ts"),
|
|
]
|
|
.join("\n");
|
|
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert", "window.confirm", "window.prompt"] {
|
|
assert!(!frontend.contains(forbidden), "forbidden frontend primitive present: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_frontend_tracing_covers_navigation_buttons_tabs_controls_refresh_and_ipc() {
|
|
let main = include_str!("../frontend/ts/main.ts");
|
|
let invoke = include_str!("../frontend/ts/invoke.ts");
|
|
assert!(main.contains("navigation button clicked"));
|
|
assert!(main.contains("diagnostics refresh button clicked"));
|
|
assert!(main.contains("generic button clicked"));
|
|
assert!(main.contains("tab control clicked"));
|
|
assert!(main.contains("tab activated"));
|
|
assert!(main.contains("interactive control changed"));
|
|
assert!(main.contains("view activated"));
|
|
assert!(invoke.contains("IPC command requested"));
|
|
assert!(invoke.contains("IPC command completed"));
|
|
assert!(invoke.contains("IPC command failed"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_store_desk_development_logging_explicitly_selects_supertrace() {
|
|
let constants = include_str!("../src/constants.rs");
|
|
let bootstrap = include_str!("../src/bootstrap.rs");
|
|
assert!(constants.contains("DEVELOPMENT_LOGGING_PROFILE: &str = \"supertrace\""));
|
|
assert!(bootstrap.contains("FILE_ID_COMPOSITE_KSP_APP_STORE_DESK"));
|
|
assert!(bootstrap.contains("logging_profile.profile_id() != crate::DEVELOPMENT_LOGGING_PROFILE"));
|
|
assert!(bootstrap.contains("LogFilterLevel::Trace"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_006_store_status_ipc_is_backend_neutral_and_secret_free() {
|
|
let dto = include_str!("../src/dto_common.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
assert!(dto.contains("StoreRuntimeStatusDto"));
|
|
assert!(runtime.contains("runtime_snapshot"));
|
|
assert!(runtime.contains("health().await"));
|
|
assert!(tauri.contains("store_runtime_status"));
|
|
for forbidden in ["connection_uri", "connectionUri", "postgresql://", "password", "RawPayload", "RawAccountState", "tokio_postgres"] {
|
|
assert!(!dto.contains(forbidden), "Store status DTO source contains forbidden field/material: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_transaction_rows_are_payload_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
|
let dto = include_str!("../src/dto_transaction.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let row_start = dto.find("pub(crate) struct StoreTransactionRowDto");
|
|
let response_start = dto.find("pub(crate) struct StoreTransactionQueryResponseDto");
|
|
assert!(row_start.is_some());
|
|
assert!(response_start.is_some());
|
|
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
|
let row_source = &dto[row_start..response_start];
|
|
assert!(!row_source.contains("payload_preview_hex"));
|
|
assert!(!row_source.contains("payload: "));
|
|
}
|
|
assert!(dto.contains("payload_preview_hex"));
|
|
assert!(dto.contains("payload_preview_truncated"));
|
|
assert!(runtime.contains("TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512"));
|
|
assert!(runtime.contains("std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES)"));
|
|
assert!(frontend.contains("transaction DataTables server-side query started"));
|
|
assert!(frontend.contains("transaction DataTables page changed"));
|
|
assert!(frontend.contains("transaction DataTables page length changed"));
|
|
assert!(frontend.contains("transaction DataTables redraw completed"));
|
|
assert!(frontend.contains("transaction detail opened"));
|
|
assert!(frontend.contains("transaction detail closed"));
|
|
for forbidden in [
|
|
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMin",
|
|
"frontendDebug(\"main\", \"Store Desk transaction Store filter changed\", { slotMax",
|
|
"frontendDebug(\"main\", \"Store Desk transaction detail opened\", { signature",
|
|
"payloadPreviewHex:",
|
|
] {
|
|
assert!(!frontend.contains(forbidden), "transaction business/filter value leaked into frontend tracing: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_transaction_request_rejects_datatables_search_order_draw_and_physical_material() {
|
|
let dto = include_str!("../src/dto_transaction.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "connection_uri", "postgresql://", "endpoint"] {
|
|
assert!(!dto.contains(forbidden), "forbidden DataTables/physical request material present: {forbidden}");
|
|
}
|
|
assert!(runtime.contains("25 | 50 | 100"));
|
|
assert!(runtime.contains("inspection_page_from_request(request.offset.as_str(), request.limit)"));
|
|
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
|
assert!(runtime.contains("RawSlotRange::new"));
|
|
assert!(runtime.contains("\"ascending\" =>"));
|
|
assert!(runtime.contains("\"descending\" =>"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_fix_003_copy_tracing_is_field_id_only_without_value_material() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
for allowed in [
|
|
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId })"#,
|
|
r#"frontendError("main", "Store Desk long text copy failed", { fieldId })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId })"#,
|
|
] {
|
|
assert!(frontend.contains(allowed), "expected field-id-only copy tracing call is missing: {allowed}");
|
|
}
|
|
for forbidden in [
|
|
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy requested", { value, fieldId })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy requested", { value })"#,
|
|
r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#,
|
|
r#"frontendError("main", "Store Desk long text copy failed", { value, fieldId })"#,
|
|
r#"frontendError("main", "Store Desk long text copy failed", { value })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy completed", { value, fieldId })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy completed", { value })"#,
|
|
] {
|
|
assert!(!frontend.contains(forbidden), "copy tracing leaked copied value material: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_007_fix_002_payload_preview_copy_never_logs_preview_material() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
assert!(frontend.contains(r#"payloadPreviewCopy.dataset.copyField = "transaction-payload-preview""#));
|
|
assert!(!frontend.contains(r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#));
|
|
assert!(!frontend.contains(r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#));
|
|
assert!(!frontend.contains(r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_account_rows_are_data_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
|
let dto = include_str!("../src/dto_account.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let row_start = dto.find("pub(crate) struct StoreAccountRowDto");
|
|
let response_start = dto.find("pub(crate) struct StoreAccountQueryResponseDto");
|
|
assert!(row_start.is_some());
|
|
assert!(response_start.is_some());
|
|
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
|
let row_source = &dto[row_start..response_start];
|
|
assert!(!row_source.contains("data_preview_hex"));
|
|
assert!(!row_source.contains("pub(crate) data:"));
|
|
}
|
|
assert!(dto.contains("data_preview_hex"));
|
|
assert!(dto.contains("data_preview_truncated"));
|
|
assert!(runtime.contains("ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512"));
|
|
assert!(runtime.contains("std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES)"));
|
|
for required in [
|
|
"account DataTables page changed",
|
|
"account DataTables page length changed",
|
|
"account DataTables redraw completed",
|
|
"account detail opened",
|
|
"account detail closed",
|
|
] {
|
|
assert!(frontend.contains(required), "missing account tracing event: {required}");
|
|
}
|
|
for forbidden in [
|
|
r#"frontendDebug("main", "Store Desk account Store filter changed", { pubkey"#,
|
|
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMin"#,
|
|
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMax"#,
|
|
r#"frontendDebug("main", "Store Desk account detail opened", { pubkey"#,
|
|
"dataPreviewHex:",
|
|
] {
|
|
assert!(!frontend.contains(forbidden), "account business/filter value leaked into frontend tracing: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_account_request_rejects_datatables_search_order_draw_network_and_physical_material() {
|
|
let dto = include_str!("../src/dto_account.rs");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "network:", "connection_uri", "postgresql://", "endpoint"] {
|
|
assert!(!dto.contains(forbidden), "forbidden DataTables/physical account request material present: {forbidden}");
|
|
}
|
|
assert!(runtime.contains("25 | 50 | 100"));
|
|
assert!(runtime.contains("inspection_page_from_request(request.offset.as_str(), request.limit)"));
|
|
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
|
assert!(runtime.contains("parse_optional_pubkey(request.pubkey.as_deref())"));
|
|
assert!(runtime.contains("RawSlotRange::new"));
|
|
assert!(runtime.contains("request.pubkey.parse::<ksp_store_lib::Pubkey>()"));
|
|
assert!(runtime.contains("decode_hex_32(request.state_hash.as_str())"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_008_account_copy_tracing_never_logs_identity_or_data_material() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
for required in ["account-pubkey", "account-state-hash", "account-owner", "account-data-preview"] {
|
|
assert!(frontend.contains(required), "missing account copy field id: {required}");
|
|
}
|
|
for forbidden in [
|
|
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#,
|
|
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#,
|
|
r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#,
|
|
] {
|
|
assert!(!frontend.contains(forbidden), "account copy tracing leaked copied value material: {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_010_observation_ipc_and_tracing_keep_entity_values_and_source_bytes_out_of_logs() {
|
|
let dto = include_str!("../src/dto_observation.rs");
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let tauri = include_str!("../src/tauri.rs");
|
|
for forbidden in ["draw:", "search:", "order:", "columns:", "cursor:", "network:"] {
|
|
assert!(!dto.contains(forbidden), "observation IPC request leaked DataTables/physical field: {forbidden}");
|
|
}
|
|
for forbidden in ["source_payload_bytes", "payload_preview", "data_preview", "raw_bytes"] {
|
|
assert!(!dto.contains(forbidden), "observation IPC leaked raw/source bytes: {forbidden}");
|
|
}
|
|
assert!(frontend.contains(r#"Store Desk transaction observation DataTables server-side query started", { pageLength: request.limit }"#));
|
|
assert!(frontend.contains(r#"Store Desk account observation DataTables server-side query started", { pageLength: request.limit }"#));
|
|
assert!(!frontend.contains(r#"query started", { signature"#));
|
|
assert!(!frontend.contains(r#"query started", { pubkey"#));
|
|
assert!(!frontend.contains(r#"query completed", { observationKey"#));
|
|
let transaction_command = tauri.find("fn store_query_transaction_observations");
|
|
let account_command = tauri.find("fn store_query_account_observations");
|
|
assert!(transaction_command.is_some());
|
|
assert!(account_command.is_some());
|
|
if let (std::option::Option::Some(transaction_start), std::option::Option::Some(account_start)) = (transaction_command, account_command) {
|
|
let transaction_source = &tauri[transaction_start..];
|
|
let account_source = &tauri[account_start..transaction_start];
|
|
assert!(transaction_source.contains("project_command_error"));
|
|
assert!(transaction_source.contains(r#""store_query_transaction_observations""#));
|
|
assert!(transaction_source.contains("crate::TRACING_DOMAIN_STORE"));
|
|
assert!(account_source.contains("project_command_error"));
|
|
assert!(account_source.contains(r#""store_query_account_observations""#));
|
|
assert!(account_source.contains("crate::TRACING_DOMAIN_STORE"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pre_010_observation_page_hardening_rejects_hostile_draw_length_and_unsafe_exact_counts() {
|
|
let frontend = include_str!("../frontend/ts/main.ts");
|
|
let runtime = include_str!("../src/store_runtime.rs");
|
|
assert!(frontend.contains("Number.isSafeInteger(page.draw)"));
|
|
assert!(frontend.contains("Number.isSafeInteger(page.start)"));
|
|
assert!(frontend.contains("[25, 50, 100].includes(page.length)"));
|
|
assert!(frontend.contains("Number.MAX_SAFE_INTEGER"));
|
|
assert!(frontend.contains("exact count exceeds frontend safe integer domain"));
|
|
assert!(runtime.contains("inspection_page_from_request"));
|
|
assert!(runtime.contains("request.offset.as_str(), request.limit"));
|
|
assert!(runtime.contains("offset_text.parse::<u64>()"));
|
|
}
|