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
15 changes: 15 additions & 0 deletions .github/workflows/e2e-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

1 change: 1 addition & 0 deletions DOCUMENTATION_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
29 changes: 29 additions & 0 deletions docs/PACKAGING_AND_INSTALL.md
Original file line number Diff line number Diff line change
@@ -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
```
18 changes: 15 additions & 3 deletions tooling/sanctifier-cli/src/commands/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,14 @@ pub(crate) fn run_analysis(args: AnalyzeArgs) -> anyhow::Result<bool> {
}

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<PathBuf> = 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();
Expand Down Expand Up @@ -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
}

Expand Down
19 changes: 1 addition & 18 deletions tooling/sanctifier-cli/src/commands/verify_deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ pub fn exec(args: VerifyDeploymentArgs) -> anyhow::Result<()> {
}
}

fn build_and_hash(source: &std::path::Path) -> anyhow::Result<String> {
/// Build the contract in release mode and return its SHA-256 hex digest.
fn build_and_hash(source: &std::path::Path) -> anyhow::Result<String> {
// Try `stellar contract build` first (Stellar CLI ≥ 0.9), fall back to cargo directly
Expand Down Expand Up @@ -184,7 +183,6 @@ fn fetch_remote_hash(contract_id: &str, network: &str) -> anyhow::Result<String>
network,
"--output-file",
])
.args(["contract", "fetch", "--id", contract_id, "--network", network, "--output-file"])
.arg(&out_path)
.status();

Expand All @@ -193,8 +191,6 @@ fn fetch_remote_hash(contract_id: &str, network: &str) -> anyhow::Result<String>
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));
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions tooling/sanctifier-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions tooling/sanctifier-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
42 changes: 42 additions & 0 deletions tooling/sanctifier-cli/tests/completions_tests.rs
Original file line number Diff line number Diff line change
@@ -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"));
}
Loading