diff --git a/.github/workflows/e2e-coverage.yml b/.github/workflows/e2e-coverage.yml index 89fc32fc..b5f755d9 100644 --- a/.github/workflows/e2e-coverage.yml +++ b/.github/workflows/e2e-coverage.yml @@ -65,3 +65,18 @@ jobs: - name: Run action integration tests run: python -m unittest discover -s tests/action -p "test_*.py" -v + + backwards-compatibility: + name: Backwards Compatibility Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master (stable) + with: + toolchain: stable + + - name: Run versioning and backwards compat tests + run: cargo test -p sanctifier-cli --test versioning_tests + diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md index 8f563bf0..2c667d1e 100644 --- a/DOCUMENTATION_INDEX.md +++ b/DOCUMENTATION_INDEX.md @@ -208,6 +208,7 @@ - Threat model and operational guarantees: [docs/release-artifacts-threat-model.md](docs/release-artifacts-threat-model.md) - How to verify a downloaded artifact: [docs/provenance-verification.md](docs/provenance-verification.md) - Canonical artifact list: [data/release-manifest.json](data/release-manifest.json) +- Packaging and Installation Guide: [docs/PACKAGING_AND_INSTALL.md](docs/PACKAGING_AND_INSTALL.md) --- diff --git a/docs/PACKAGING_AND_INSTALL.md b/docs/PACKAGING_AND_INSTALL.md new file mode 100644 index 00000000..712c5d48 --- /dev/null +++ b/docs/PACKAGING_AND_INSTALL.md @@ -0,0 +1,29 @@ +# Packaging and Installation Guide + +## Building from Source + +To build the `sanctifier-cli` from source: + +1. Ensure you have the latest stable Rust installed. +2. Install the `z3` theorem prover (required for SMT solving capabilities). +3. Run `cargo build --release -p sanctifier-cli`. +4. The compiled binary will be available at `target/release/sanctifier`. + +## Distribution + +When packaging `sanctifier` for distribution, note the following dependencies: +- Z3 must be dynamically or statically linked. For static linking, follow the `z3-sys` static compilation instructions. + +## Installation via Cargo + +You can install the CLI directly from crates.io (once published): +```sh +cargo install sanctifier-cli +``` + +## Running Backwards Compatibility Tests + +We maintain backwards compatibility for standard output and flags. Run the versioning and compatibility suite via: +```sh +cargo test -p sanctifier-cli --test versioning_tests +``` diff --git a/tooling/sanctifier-cli/src/commands/analyze.rs b/tooling/sanctifier-cli/src/commands/analyze.rs index 4ce6e071..63b6851f 100644 --- a/tooling/sanctifier-cli/src/commands/analyze.rs +++ b/tooling/sanctifier-cli/src/commands/analyze.rs @@ -180,14 +180,14 @@ pub(crate) fn run_analysis(args: AnalyzeArgs) -> anyhow::Result { } let start = Instant::now(); - let config = load_config(path); + let config = load_config(&path); let telemetry_enabled = config.telemetry; // When a single file is given, scan only that file — not its parent directory. let rs_files: Vec = if path.is_file() { vec![path.clone()] } else { - collect_rs_files(path, &config.ignore_paths) + collect_rs_files(&path, &config.ignore_paths) }; let registry = RuleRegistry::with_default_rules(); @@ -701,15 +701,27 @@ fn sha256_hex(content: &str) -> String { #[cfg(not(windows))] pub(crate) fn normalize_cli_path(p: PathBuf) -> PathBuf { let s = p.to_string_lossy(); - if s.contains('\\') { + let sanitized = if s.contains('\\') { PathBuf::from(s.replace('\\', "/")) } else { p + }; + + // Prevent directory traversal escapes (security default) + if sanitized.components().any(|c| matches!(c, std::path::Component::ParentDir)) { + eprintln!("Warning: Path traversal detected. Falling back to current directory."); + return PathBuf::from("."); } + sanitized } #[cfg(windows)] pub(crate) fn normalize_cli_path(p: PathBuf) -> PathBuf { + // Prevent directory traversal escapes (security default) + if p.components().any(|c| matches!(c, std::path::Component::ParentDir)) { + eprintln!("Warning: Path traversal detected. Falling back to current directory."); + return PathBuf::from("."); + } p } diff --git a/tooling/sanctifier-cli/src/commands/verify_deployment.rs b/tooling/sanctifier-cli/src/commands/verify_deployment.rs index e349b9b9..923a3ef1 100644 --- a/tooling/sanctifier-cli/src/commands/verify_deployment.rs +++ b/tooling/sanctifier-cli/src/commands/verify_deployment.rs @@ -110,7 +110,6 @@ pub fn exec(args: VerifyDeploymentArgs) -> anyhow::Result<()> { } } -fn build_and_hash(source: &std::path::Path) -> anyhow::Result { /// Build the contract in release mode and return its SHA-256 hex digest. fn build_and_hash(source: &std::path::Path) -> anyhow::Result { // Try `stellar contract build` first (Stellar CLI ≥ 0.9), fall back to cargo directly @@ -184,7 +183,6 @@ fn fetch_remote_hash(contract_id: &str, network: &str) -> anyhow::Result network, "--output-file", ]) - .args(["contract", "fetch", "--id", contract_id, "--network", network, "--output-file"]) .arg(&out_path) .status(); @@ -193,8 +191,6 @@ fn fetch_remote_hash(contract_id: &str, network: &str) -> anyhow::Result let bytes = std::fs::read(&out_path).with_context(|| { format!("failed to read fetched WASM: {}", out_path.display()) })?; - let bytes = std::fs::read(&out_path) - .with_context(|| format!("failed to read fetched WASM: {}", out_path.display()))?; let _ = std::fs::remove_file(&out_path); return Ok(sha256_hex(&bytes)); } @@ -233,8 +229,6 @@ fn sha256_hex(data: &[u8]) -> String { W(0x9b05688c), W(0x1f83d9ab), W(0x5be0cd19), - W(0x6a09e667), W(0xbb67ae85), W(0x3c6ef372), W(0xa54ff53a), - W(0x510e527f), W(0x9b05688c), W(0x1f83d9ab), W(0x5be0cd19), ]; let bit_len = (data.len() as u64).wrapping_mul(8); @@ -251,11 +245,7 @@ fn sha256_hex(data: &[u8]) -> String { w[i] = W(u32::from_be_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap())); } for i in 16..64 { - let s0 = w[i - 15].0.rotate_right(7) - ^ w[i - 15].0.rotate_right(18) - ^ (w[i - 15].0 >> 3); - let s1 = - w[i - 2].0.rotate_right(17) ^ w[i - 2].0.rotate_right(19) ^ (w[i - 2].0 >> 10); + let s0 = w[i - 15].0.rotate_right(7) ^ w[i - 15].0.rotate_right(18) ^ (w[i - 15].0 >> 3); let s1 = w[i - 2].0.rotate_right(17) ^ w[i - 2].0.rotate_right(19) ^ (w[i - 2].0 >> 10); w[i] = w[i - 16] + W(s0) + w[i - 7] + W(s1); @@ -286,13 +276,6 @@ fn sha256_hex(data: &[u8]) -> String { h[5] += f; h[6] += g; h[7] += hh; - hh = g; g = f; f = e; - e = d + temp1; - d = c; c = b; b = a; - a = temp1 + temp2; - } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; - h[4] += e; h[5] += f; h[6] += g; h[7] += hh; } h.iter() diff --git a/tooling/sanctifier-cli/src/main.rs b/tooling/sanctifier-cli/src/main.rs index 7e1c2518..677ecfe1 100644 --- a/tooling/sanctifier-cli/src/main.rs +++ b/tooling/sanctifier-cli/src/main.rs @@ -6,8 +6,6 @@ use std::io; use sanctifier_cli::commands; use sanctifier_cli::logging; -use sanctifier_cli::telemetry; -use sanctifier_cli::vulndb; #[derive(Parser)] #[command( diff --git a/tooling/sanctifier-cli/tests/cli_tests.rs b/tooling/sanctifier-cli/tests/cli_tests.rs index 4e05919c..159293c1 100644 --- a/tooling/sanctifier-cli/tests/cli_tests.rs +++ b/tooling/sanctifier-cli/tests/cli_tests.rs @@ -1201,6 +1201,7 @@ fn test_top_level_help_lists_all_core_subcommands() { "top-level --help should list '{cmd}' but didn't" ); } +} // ── #517: Config file resolution precedence ─────────────────────────────────── /// The config file in the same directory as the analysed file is used (nearest wins). diff --git a/tooling/sanctifier-cli/tests/completions_tests.rs b/tooling/sanctifier-cli/tests/completions_tests.rs new file mode 100644 index 00000000..7070572b --- /dev/null +++ b/tooling/sanctifier-cli/tests/completions_tests.rs @@ -0,0 +1,42 @@ +use assert_cmd::Command; +use predicates::prelude::*; + +#[test] +fn test_completions_bash() { + let mut cmd = Command::cargo_bin("sanctifier").unwrap(); + cmd.arg("completions") + .arg("bash") + .assert() + .success() + .stdout(predicate::str::contains("_sanctifier()")); +} + +#[test] +fn test_completions_zsh() { + let mut cmd = Command::cargo_bin("sanctifier").unwrap(); + cmd.arg("completions") + .arg("zsh") + .assert() + .success() + .stdout(predicate::str::contains("#compdef sanctifier")); +} + +#[test] +fn test_completions_fish() { + let mut cmd = Command::cargo_bin("sanctifier").unwrap(); + cmd.arg("completions") + .arg("fish") + .assert() + .success() + .stdout(predicate::str::contains("complete -c sanctifier")); +} + +#[test] +fn test_completions_powershell() { + let mut cmd = Command::cargo_bin("sanctifier").unwrap(); + cmd.arg("completions") + .arg("powershell") + .assert() + .success() + .stdout(predicate::str::contains("Register-ArgumentCompleter")); +}