From 7114cdd9bba4b02b95a9e6969ccde65188f954b6 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Mon, 5 May 2025 11:53:57 +0530 Subject: [PATCH 01/27] feat: Initial directory strcture setup --- Cargo.toml | 7 +++++++ README.md | 0 src/cli.rs | 7 +++++++ src/main.rs | 0 src/reporting/json.rs | 0 src/utils/logging.rs | 0 6 files changed, 14 insertions(+) create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/cli.rs create mode 100644 src/main.rs create mode 100644 src/reporting/json.rs create mode 100644 src/utils/logging.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8580bb6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rustybox" +version = "0.1.0" +edition = "2025" +license = "Apache-2.0" +repository = "https://github.com/Aswinr24/RustyBox" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8580bb6 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,7 @@ +[package] +name = "rustybox" +version = "0.1.0" +edition = "2025" +license = "Apache-2.0" +repository = "https://github.com/Aswinr24/RustyBox" + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/reporting/json.rs b/src/reporting/json.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/logging.rs b/src/utils/logging.rs new file mode 100644 index 0000000..e69de29 From 3edd8b7108b28f8a626568602a2950f3418086b3 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Mon, 5 May 2025 14:44:02 +0530 Subject: [PATCH 02/27] feat: standalone directories for static and dynamic analysis --- src/dynamic_analysis/mod.rs | 0 src/static_analysis/mod.rs | 0 src/utils/mod.rs | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/dynamic_analysis/mod.rs create mode 100644 src/static_analysis/mod.rs create mode 100644 src/utils/mod.rs diff --git a/src/dynamic_analysis/mod.rs b/src/dynamic_analysis/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/static_analysis/mod.rs b/src/static_analysis/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..e69de29 From 56ffdbff158f0ba5e9b19809e39935c2cd879615 Mon Sep 17 00:00:00 2001 From: ritishab0209 Date: Mon, 5 May 2025 15:47:29 +0530 Subject: [PATCH 03/27] Initial commit --- src/static_analysis/decompilation.rs | 11 +++++++++++ src/static_analysis/disassembly.rs | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/static_analysis/decompilation.rs create mode 100644 src/static_analysis/disassembly.rs diff --git a/src/static_analysis/decompilation.rs b/src/static_analysis/decompilation.rs new file mode 100644 index 0000000..9048558 --- /dev/null +++ b/src/static_analysis/decompilation.rs @@ -0,0 +1,11 @@ +use r2pipe::R2Pipe; + +/// Use Radare2's native decompiler (pdd) and return the decompiled output as a String +pub fn decompile_binary(file_path: &str) -> Result> { + let mut r2 = R2Pipe::spawn(file_path, None)?; + r2.cmd("aaa")?; // Analyze all + let decompiled_code = r2.cmd("pdd")?; // pdd = pseudo-code decompiler + + r2.close(); + Ok(decompiled_code) +} diff --git a/src/static_analysis/disassembly.rs b/src/static_analysis/disassembly.rs new file mode 100644 index 0000000..4579553 --- /dev/null +++ b/src/static_analysis/disassembly.rs @@ -0,0 +1,23 @@ +use r2pipe::R2Pipe; +use serde_json::Value; + +/// Disassemble the binary and return disassembly of `count` instructions as a String +pub fn disassemble_binary(file_path: &str, count: u32) -> Result> { + let mut r2 = R2Pipe::spawn(file_path, None)?; + r2.cmd("aaa")?; // Analyze everything + + let command = format!("pdj {}", count); + let json_output = r2.cmd(&command)?; + let instructions: Vec = serde_json::from_str(&json_output)?; + + let mut disassembly_output = String::new(); + for instr in instructions { + let offset = instr["offset"].as_u64().unwrap_or(0); + let mnemonic = instr["mnemonic"].as_str().unwrap_or(""); + let opcode = instr["opcode"].as_str().unwrap_or(""); + disassembly_output.push_str(&format!("{:#x}:\t{}\t{}\n", offset, mnemonic, opcode)); + } + + r2.close(); + Ok(disassembly_output) +} From d2145cb21c2e48849d6f3adf8900b19116f511bb Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Tue, 6 May 2025 02:00:36 +0530 Subject: [PATCH 04/27] fix: rust edition --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8580bb6..6eab764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rustybox" version = "0.1.0" -edition = "2025" +edition = "2024" license = "Apache-2.0" repository = "https://github.com/Aswinr24/RustyBox" From 447f886d7dd8bd940d924149da68c81059adbec2 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Tue, 6 May 2025 02:01:45 +0530 Subject: [PATCH 05/27] ci: workflows for testing build, security audit and linting --- .github/workflows/build-test.yml | 26 ++++++++++++++++++++++++++ .github/workflows/lint.yml | 21 +++++++++++++++++++++ .github/workflows/sast.yml | 15 +++++++++++++++ .github/workflows/security-audit.yml | 17 +++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 .github/workflows/build-test.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/sast.yml create mode 100644 .github/workflows/security-audit.yml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..d2856b5 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,26 @@ +name: Build & Test + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust: [stable, beta] + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - name: Build + run: cargo build --all-features + - name: Run tests + run: cargo test --all-features diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8a276d3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + pull_request: + branches: [main] + +jobs: + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy, rustfmt + - name: Rustfmt Check + run: cargo fmt -- --check + - name: Clippy Check + run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml new file mode 100644 index 0000000..31b90b5 --- /dev/null +++ b/.github/workflows/sast.yml @@ -0,0 +1,15 @@ +name: Static Analysis + +on: + pull_request: + branches: [main] + +jobs: + sast: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install cargo-deny + run: cargo install --locked cargo-deny + - name: Run cargo-deny + run: cargo deny check bans licenses sources diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..2a37326 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,17 @@ +name: Cargo Audit + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install cargo-audit + run: cargo install cargo-audit --locked + - name: Run cargo audit + run: cargo audit From 81dcc3d4a657fe21ccd349f124f96356c13fabf7 Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Tue, 6 May 2025 18:29:38 +0530 Subject: [PATCH 06/27] Create metadata.rs --- src/static_analysis/metadata.rs | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/static_analysis/metadata.rs diff --git a/src/static_analysis/metadata.rs b/src/static_analysis/metadata.rs new file mode 100644 index 0000000..a5498fc --- /dev/null +++ b/src/static_analysis/metadata.rs @@ -0,0 +1,67 @@ +use goblin::Object; +use std::fs; + +#[derive(Debug)] +pub struct BinaryMetadata { + pub format: String, + pub entry_point: Option, + pub sections: Option, + pub program_headers: Option, + pub machine: Option, + pub image_base: Option, + pub is_64: Option, + pub load_commands: Option, + pub cpu_type: Option, + pub arch_count: Option, +} + +pub fn extract_metadata(file_path: &str) -> Result> { + let buffer = fs::read(file_path)?; + let mut metadata = BinaryMetadata { + format: String::new(), + entry_point: None, + sections: None, + program_headers: None, + machine: None, + image_base: None, + is_64: None, + load_commands: None, + cpu_type: None, + arch_count: None, + }; + + match Object::parse(&buffer)? { + Object::Elf(elf) => { + metadata.format = "ELF".to_string(); + metadata.entry_point = Some(elf.entry); + metadata.sections = Some(elf.section_headers.len()); + metadata.program_headers = Some(elf.program_headers.len()); + metadata.machine = Some(format!("{:?}", elf.header.e_machine)); + } + Object::PE(pe) => { + metadata.format = "PE".to_string(); + metadata.entry_point = Some(pe.entry); + metadata.sections = Some(pe.sections.len()); + metadata.image_base = Some(pe.image_base); + metadata.machine = Some(format!("{:?}", pe.header.coff_header.machine)); + } + Object::Mach(mach_obj) => { + metadata.format = "Mach-O".to_string(); + match mach_obj { + goblin::mach::Mach::Binary(macho) => { + metadata.is_64 = Some(macho.is_64); + metadata.load_commands = Some(macho.header.ncmds); + metadata.cpu_type = Some(format!("{:?}", macho.header.cputype)); + } + goblin::mach::Mach::Fat(fat) => { + metadata.arch_count = Some(fat.narches); + } + } + } + _ => { + metadata.format = "Unknown".to_string(); + } + } + + Ok(metadata) +} From caf2d823ade290ff598e6df3515a49e07522a922 Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Tue, 6 May 2025 18:33:22 +0530 Subject: [PATCH 07/27] Create callgraph.rs --- src/static_analysis/callgraph.rs | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/static_analysis/callgraph.rs diff --git a/src/static_analysis/callgraph.rs b/src/static_analysis/callgraph.rs new file mode 100644 index 0000000..f19edac --- /dev/null +++ b/src/static_analysis/callgraph.rs @@ -0,0 +1,42 @@ +use std::fs; +use std::process::{Command, Stdio}; +use std::path::Path; + +pub fn analyze_callgraph(binary_path: &str) -> Result> { + let dot_path = "callgraph.dot"; + + // Step 1: Run radare2 to analyze and export the call graph in DOT format + let r2_command = format!("e bin.relocs.apply=true; e bin.cache=true; aa; agfd > {}", dot_path); + let status = Command::new("r2") + .args(["-Aqc", &r2_command, binary_path]) + .status()?; + + if !status.success() { + return Err("Failed to generate callgraph.dot using radare2".into()); + } + + if !Path::new(dot_path).exists() { + return Err("DOT file not found after radare2 run".into()); + } + + // Step 2: Convert DOT to ASCII using graph-easy + let output = Command::new("graph-easy") + .arg(dot_path) + .stdout(Stdio::piped()) + .output()?; + + // Cleanup DOT file regardless of result + fs::remove_file(dot_path).ok(); + + if output.status.success() { + let ascii_graph = String::from_utf8_lossy(&output.stdout).to_string(); + Ok(ascii_graph) + } else { + let err_msg = String::from_utf8_lossy(&output.stderr).to_string(); + Err(format!( + "graph-easy error:\n{}\nHint: Install it using `cpanm Graph::Easy`", + err_msg + ) + .into()) + } +} From 4bb9291c5550649852910ffa5b00b6242495f24c Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Tue, 6 May 2025 19:38:28 +0530 Subject: [PATCH 08/27] chore(ci): replace actions-rs/toolchain with dtolnay/rust-toolchain --- .github/workflows/build-test.yml | 4 +--- .github/workflows/lint.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d2856b5..4fb22c7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -15,11 +15,9 @@ jobs: rust: [stable, beta] steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: - profile: minimal toolchain: ${{ matrix.rust }} - override: true - name: Build run: cargo build --all-features - name: Run tests diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a276d3..9ba6043 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,11 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: - profile: minimal toolchain: stable - override: true components: clippy, rustfmt - name: Rustfmt Check run: cargo fmt -- --check From c9d35010fc4c76dc3b0c7bc3259ca6dd03c8e355 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Wed, 7 May 2025 01:11:41 +0530 Subject: [PATCH 09/27] feat: capa for signature based analysis of malware --- src/static_analysis/signature.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/static_analysis/signature.rs diff --git a/src/static_analysis/signature.rs b/src/static_analysis/signature.rs new file mode 100644 index 0000000..2db6e7d --- /dev/null +++ b/src/static_analysis/signature.rs @@ -0,0 +1,23 @@ +use std::process::Command; + +pub fn run_capa_raw(file_path: &str, output_format: &str) -> Result> { + let mut cmd = Command::new("capa"); + cmd.arg(file_path); + + match output_format { + "json" => cmd.arg("-j"), + "vverbose" => cmd.arg("-vv"), + "verbose" => cmd.arg("-v"), + _ => cmd.arg("-v"), // default to verbose + }; + + let output = cmd.output()?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr).into_owned(); + return Err(format!("CAPA analysis failed: {}", error_msg).into()); + } + + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + Ok(stdout) +} From 379b9500a16edd17fd0f8731e9a7626f33df0e19 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Fri, 9 May 2025 19:09:07 +0530 Subject: [PATCH 10/27] feat: cli interface for rustybox --- Cargo.lock | 1202 ++++++++++++++++++++++++++ Cargo.toml | 15 + src/cli.rs | 530 +++++++++++- src/main.rs | 10 + src/static_analysis/callgraph.rs | 30 +- src/static_analysis/decompilation.rs | 65 +- src/static_analysis/disassembly.rs | 43 +- src/static_analysis/metadata.rs | 9 +- src/static_analysis/mod.rs | 11 + src/utils/logging.rs | 43 + 10 files changed, 1920 insertions(+), 38 deletions(-) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0502943 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1202 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ansi_term" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "ascii_tree" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.7", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dot-parser" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cce83b336cd8d9b2718f4e8228a185de6e7a1cfe34f237e8c4acaf1c4141630" +dependencies = [ + "either", + "pest", + "pest_derive", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "goblin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "logging" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "461a8beca676e8ab1bd468c92e9b4436d6368e11e96ae038209e520cfe665e46" +dependencies = [ + "ansi_term", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2pipe" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63443078dfdb83f59820ed161863e38df38e8c6a2c3b85733ebac1bb65b4af2b" +dependencies = [ + "libc", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "rustybox" +version = "0.1.0" +dependencies = [ + "ascii_tree", + "clap", + "colored", + "crossterm 0.29.0", + "dot-parser", + "goblin", + "log", + "logging", + "petgraph", + "r2pipe", + "ratatui", + "serde_json", + "simplelog", + "termcolor", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 6eab764..c97e711 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,18 @@ edition = "2024" license = "Apache-2.0" repository = "https://github.com/Aswinr24/RustyBox" +[dependencies] +ascii_tree = "0.1.1" +clap = "4.5.37" +colored = "3.0.0" +goblin = "0.9.3" +log = "0.4.27" +logging = "0.1.0" +r2pipe = "0.7.0" +serde_json = "1.0.140" +simplelog = "0.12.2" +termcolor = "1.4.1" +dot-parser = "0.5.1" +petgraph = "0.6.4" +ratatui = "0.29.0" +crossterm = "0.29.0" diff --git a/src/cli.rs b/src/cli.rs index 8580bb6..5dd9dcd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,525 @@ -[package] -name = "rustybox" -version = "0.1.0" -edition = "2025" -license = "Apache-2.0" -repository = "https://github.com/Aswinr24/RustyBox" +use clap::{Arg, Command}; +use colored::Colorize; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, List, ListItem, Paragraph, Tabs}, + Terminal, +}; +use std::{ + io, + path::Path, + sync::mpsc, + thread, + time::{Duration, Instant}, +}; +// Import our static analysis functions +use crate::static_analysis::{analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata}; + +const RUSTYBOX_ASCII: &str = r#" + ____ _ ____ +| _ \ _ _ ___| |_ _ _| __ ) _____ __ +| |_) | | | / __| __| | | | _ \ / _ \ \/ / +| _ <| |_| \__ \ |_| |_| | |_) | (_) > < +|_| \_\\__,_|___/\__|\__, |____/ \___/_/\_\ + |___/ + Static Binary Analysis Tool +"#; + +enum AnalysisMode { + Metadata, + Disassembly, + Decompile, + Callgraph, +} + +struct AnalysisResult { + metadata: Option>, + disassembly: Option>, + decompile: Option>, + callgraph: Option>, +} + +struct App { + selected_tab: usize, + analysis_results: AnalysisResult, + file_path: String, + instr_count: u32, + verbose: bool, +} + +impl App { + fn new(file_path: String, instr_count: u32, verbose: bool) -> Self { + Self { + selected_tab: 0, + analysis_results: AnalysisResult { + metadata: None, + disassembly: None, + decompile: None, + callgraph: None, + }, + file_path, + instr_count, + verbose, + } + } + + fn tab_titles(&self) -> Vec { + vec![ + "Metadata".to_string(), + "Disassembly".to_string(), + "Decompile".to_string(), + "Callgraph".to_string(), + ] + } + + fn next_tab(&mut self) { + self.selected_tab = (self.selected_tab + 1) % 4; + } + + fn previous_tab(&mut self) { + self.selected_tab = (self.selected_tab + 3) % 4; + } + + fn get_current_result_text(&self) -> String { + match self.selected_tab { + 0 => match &self.analysis_results.metadata { + Some(Ok(text)) => text.clone(), + Some(Err(e)) => format!("Error: {}", e), + None => "Loading metadata...".to_string(), + }, + 1 => match &self.analysis_results.disassembly { + Some(Ok(text)) => text.clone(), + Some(Err(e)) => format!("Error: {}", e), + None => "Loading disassembly...".to_string(), + }, + 2 => match &self.analysis_results.decompile { + Some(Ok(text)) => text.clone(), + Some(Err(e)) => format!("Error: {}", e), + None => "Loading decompiled code...".to_string(), + }, + 3 => match &self.analysis_results.callgraph { + Some(Ok(text)) => text.clone(), + Some(Err(e)) => format!("Error: {}", e), + None => "Loading callgraph...".to_string(), + }, + _ => "Unknown tab".to_string(), + } + } +} + +pub fn run() -> Result<(), Box> { + let matches = create_cli().get_matches(); + + let file_path = matches.get_one::("FILE").unwrap(); + + // Check if file exists + if !Path::new(file_path).exists() { + return Err(format!("File does not exist: {}", file_path).into()); + } + + // If help was requested or no GUI is requested, run in standard CLI mode + if matches.get_flag("no-tui") { + return run_standard_cli(matches); + } + + // Otherwise, run the TUI interface + run_tui( + file_path.clone(), + matches.get_one::("count").copied().unwrap_or(20), + matches.get_flag("verbose"), + ) +} + +fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box> { + let file_path = matches.get_one::("FILE").unwrap(); + + println!("{}", RUSTYBOX_ASCII.cyan().bold()); + + // Determine which analyses to run + let run_all = !matches.get_flag("disassemble") + && !matches.get_flag("metadata") + && !matches.get_flag("decompile") + && !matches.get_flag("callgraph"); + + let mut results_found = false; + + // Metadata Analysis + if run_all || matches.get_flag("metadata") { + results_found = true; + println!("{}", "\n[+] Binary Metadata Analysis".green().bold()); + println!("{}", "=========================".green()); + + match extract_metadata(file_path) { + Ok(metadata) => { + println!("Format: {}", metadata.format); + if let Some(entry) = metadata.entry_point { + println!("Entry Point: {:#x}", entry); + } + if let Some(sections) = metadata.sections { + println!("Number of Sections: {}", sections); + } + if let Some(ph) = metadata.program_headers { + println!("Program Headers: {}", ph); + } + if let Some(machine) = &metadata.machine { + println!("Machine Type: {}", machine); + } + if let Some(image_base) = metadata.image_base { + println!("Image Base: {:#x}", image_base); + } + if let Some(is_64) = metadata.is_64 { + println!("64-bit: {}", is_64); + } + if let Some(load_cmds) = metadata.load_commands { + println!("Load Commands: {}", load_cmds); + } + if let Some(cpu) = &metadata.cpu_type { + println!("CPU Type: {}", cpu); + } + if let Some(arch_count) = metadata.arch_count { + println!("Architecture Count: {}", arch_count); + } + } + Err(e) => { + eprintln!("{} {}", "[-] Error extracting metadata:".red().bold(), e); + } + } + } + + // Disassembly Analysis + if run_all || matches.get_flag("disassemble") { + results_found = true; + println!("{}", "\n[+] Disassembly".green().bold()); + println!("{}", "=============".green()); + + let instr_count = matches.get_one::("count").copied().unwrap_or(20); + + match disassemble_binary(file_path, instr_count, matches.get_flag("verbose")) { + Ok(disasm) => println!("{}", disasm), + Err(e) => eprintln!("Disassembly error: {}", e), + } + } + + // Decompilation Analysis + if run_all || matches.get_flag("decompile") { + results_found = true; + println!("{}", "\n[+] Decompiled Code".green().bold()); + println!("{}", "=================".green()); + + match decompile_binary(file_path, matches.get_flag("verbose")) { + Ok(decompiled) => { + println!("{}", decompiled); + } + Err(e) => { + eprintln!("{} {}", "[-] Error decompiling binary:".red().bold(), e); + } + } + } + + // Callgraph Analysis + if run_all || matches.get_flag("callgraph") { + results_found = true; + println!("{}", "\n[+] Function Call Graph".green().bold()); + println!("{}", "====================".green()); + + match analyze_callgraph(file_path) { + Ok(graph) => { + println!("{}", graph); + } + Err(e) => { + eprintln!("{} {}", "[-] Error generating call graph:".red().bold(), e); + } + } + } + + if !results_found { + println!( + "{}", + "No analysis was performed. Use '--help' to see available options.".yellow() + ); + } + + Ok(()) +} + +fn run_tui( + file_path: String, + instr_count: u32, + verbose: bool, +) -> Result<(), Box> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Setup app state + let mut app = App::new(file_path.clone(), instr_count, verbose); + + // Channel for background worker + let (tx, rx) = mpsc::channel(); + let file_path_clone = file_path.clone(); + let instr_count_clone = instr_count; + let verbose_clone = verbose; + + // Spawn worker thread that will perform analysis + thread::spawn(move || { + // Run all analyses in sequence + // Metadata + let metadata_result = match extract_metadata(&file_path_clone) { + Ok(metadata) => { + let mut output = String::new(); + output.push_str(&format!("Format: {}\n", metadata.format)); + if let Some(entry) = metadata.entry_point { + output.push_str(&format!("Entry Point: {:#x}\n", entry)); + } + if let Some(sections) = metadata.sections { + output.push_str(&format!("Number of Sections: {}\n", sections)); + } + if let Some(ph) = metadata.program_headers { + output.push_str(&format!("Program Headers: {}\n", ph)); + } + if let Some(machine) = &metadata.machine { + output.push_str(&format!("Machine Type: {}\n", machine)); + } + if let Some(image_base) = metadata.image_base { + output.push_str(&format!("Image Base: {:#x}\n", image_base)); + } + if let Some(is_64) = metadata.is_64 { + output.push_str(&format!("64-bit: {}\n", is_64)); + } + if let Some(load_cmds) = metadata.load_commands { + output.push_str(&format!("Load Commands: {}\n", load_cmds)); + } + if let Some(cpu) = &metadata.cpu_type { + output.push_str(&format!("CPU Type: {}\n", cpu)); + } + if let Some(arch_count) = metadata.arch_count { + output.push_str(&format!("Architecture Count: {}\n", arch_count)); + } + Ok(output) + } + Err(e) => Err(e.to_string()), + }; + tx.send((AnalysisMode::Metadata, metadata_result)).unwrap(); + + // Disassembly + let disasm_result = match disassemble_binary(&file_path_clone, instr_count_clone, verbose_clone) { + Ok(disasm) => Ok(disasm), + Err(e) => Err(e.to_string()), + }; + tx.send((AnalysisMode::Disassembly, disasm_result)).unwrap(); + + // Decompile + let decompile_result = match decompile_binary(&file_path_clone, verbose_clone) { + Ok(decompiled) => Ok(decompiled), + Err(e) => Err(e.to_string()), + }; + tx.send((AnalysisMode::Decompile, decompile_result)).unwrap(); + + // Callgraph + let callgraph_result = match analyze_callgraph(&file_path_clone) { + Ok(graph) => Ok(graph), + Err(e) => Err(e.to_string()), + }; + tx.send((AnalysisMode::Callgraph, callgraph_result)).unwrap(); + }); + + // Main loop + let tick_rate = Duration::from_millis(250); + let mut last_tick = Instant::now(); + + loop { + // Check for results from worker thread + if let Ok((mode, result)) = rx.try_recv() { + match mode { + AnalysisMode::Metadata => app.analysis_results.metadata = Some(result), + AnalysisMode::Disassembly => app.analysis_results.disassembly = Some(result), + AnalysisMode::Decompile => app.analysis_results.decompile = Some(result), + AnalysisMode::Callgraph => app.analysis_results.callgraph = Some(result), + } + } + + // Draw the UI + terminal.draw(|f| draw_ui(f, &app))?; + + // Handle input + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Right => app.next_tab(), + KeyCode::Left => app.previous_tab(), + KeyCode::Tab => app.next_tab(), + KeyCode::BackTab => app.previous_tab(), + KeyCode::Char('1') => app.selected_tab = 0, + KeyCode::Char('2') => app.selected_tab = 1, + KeyCode::Char('3') => app.selected_tab = 2, + KeyCode::Char('4') => app.selected_tab = 3, + _ => {} + } + } + } + + if last_tick.elapsed() >= tick_rate { + last_tick = Instant::now(); + } + } + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) +} + +fn draw_ui(f: &mut ratatui::Frame, app: &App) { + // Create main layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(8), // Header with ASCII art + Constraint::Length(3), // Tabs + Constraint::Min(0), // Content + Constraint::Length(1), // Help text + ]) + .split(f.area()); + + // Draw ASCII Art header + let title_text = Text::from(vec![Line::from(vec![ + Span::styled(RUSTYBOX_ASCII, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + ])]); + + let header = Paragraph::new(title_text) + .style(Style::default().fg(Color::Cyan)) + .block(Block::default()); + + f.render_widget(header, chunks[0]); + + // Draw tabs + let titles = app.tab_titles(); + let tab_titles: Vec = titles + .iter() + .map(|t| Line::from(Span::styled(t, Style::default().fg(Color::White)))) + .collect(); + + let tabs = Tabs::new(tab_titles) + .block(Block::default().borders(Borders::ALL).title("Analysis Modes")) + .select(app.selected_tab) + .style(Style::default().fg(Color::White)) + .highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ); + + f.render_widget(tabs, chunks[1]); + + // Get content based on selected tab + let content_text = app.get_current_result_text(); + let content = Paragraph::new(content_text) + .block(Block::default().borders(Borders::ALL).title(match app.selected_tab { + 0 => "Binary Metadata", + 1 => "Disassembly", + 2 => "Decompiled Code", + 3 => "Function Call Graph", + _ => "Unknown", + })) + .style(Style::default().fg(Color::White)) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, chunks[2]); + + // Help text + let help_text = Paragraph::new("Press Tab/Left/Right to switch tabs | 1-4 to select tab | q to quit") + .style(Style::default().fg(Color::DarkGray)); + + f.render_widget(help_text, chunks[3]); +} + +fn create_cli() -> Command { + Command::new("Rustybox") + .version("0.1.0") + .author("Rustybox Team") + .about("A malware analysis tool for static and dynamic analysis") + .arg( + Arg::new("FILE") + .help("Path to the binary file to analyze") + .required(true) + .index(1), + ) + .arg( + Arg::new("metadata") + .long("metadata") + .short('m') + .help("Extract metadata from the binary") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("disassemble") + .long("disassemble") + .short('d') + .help("Disassemble the binary") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("count") + .long("count") + .short('c') + .help("Number of instructions to disassemble (default: 20)") + .value_parser(clap::value_parser!(u32)) + .requires("disassemble"), + ) + .arg( + Arg::new("decompile") + .long("decompile") + .short('p') + .help("Decompile the binary to pseudo-code") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("callgraph") + .long("callgraph") + .short('g') + .help("Generate and display the function call graph") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("verbose") + .long("verbose") + .short('v') + .help("Enable verbose logging output") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("log-file") + .long("log-file") + .help("Path to log file (optional)") + .value_name("FILE"), + ) + .arg( + Arg::new("no-tui") + .long("no-tui") + .help("Run in classic command-line mode without TUI") + .action(clap::ArgAction::SetTrue), + ) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e69de29..e011e72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -0,0 +1,10 @@ +mod static_analysis; +mod cli; +mod utils; + +fn main() { + if let Err(e) = cli::run() { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} \ No newline at end of file diff --git a/src/static_analysis/callgraph.rs b/src/static_analysis/callgraph.rs index f19edac..d0c8b2a 100644 --- a/src/static_analysis/callgraph.rs +++ b/src/static_analysis/callgraph.rs @@ -1,38 +1,38 @@ use std::fs; use std::process::{Command, Stdio}; use std::path::Path; +use std::io::Write; pub fn analyze_callgraph(binary_path: &str) -> Result> { let dot_path = "callgraph.dot"; - // Step 1: Run radare2 to analyze and export the call graph in DOT format - let r2_command = format!("e bin.relocs.apply=true; e bin.cache=true; aa; agfd > {}", dot_path); - let status = Command::new("r2") - .args(["-Aqc", &r2_command, binary_path]) - .status()?; + // Step 1: Run radare2 and capture DOT callgraph output + let output = Command::new("r2") + .args(["-Aqc", "e bin.relocs.apply=true; e bin.cache=true; aa; agfd", binary_path]) + .stdout(Stdio::piped()) + .output()?; - if !status.success() { - return Err("Failed to generate callgraph.dot using radare2".into()); + if !output.status.success() { + return Err("Failed to generate callgraph using radare2".into()); } - if !Path::new(dot_path).exists() { - return Err("DOT file not found after radare2 run".into()); - } + // Write output to DOT file + fs::write(dot_path, &output.stdout)?; // Step 2: Convert DOT to ASCII using graph-easy - let output = Command::new("graph-easy") + let graph_output = Command::new("graph-easy") .arg(dot_path) .stdout(Stdio::piped()) .output()?; - // Cleanup DOT file regardless of result + // Cleanup DOT file fs::remove_file(dot_path).ok(); - if output.status.success() { - let ascii_graph = String::from_utf8_lossy(&output.stdout).to_string(); + if graph_output.status.success() { + let ascii_graph = String::from_utf8_lossy(&graph_output.stdout).to_string(); Ok(ascii_graph) } else { - let err_msg = String::from_utf8_lossy(&output.stderr).to_string(); + let err_msg = String::from_utf8_lossy(&graph_output.stderr).to_string(); Err(format!( "graph-easy error:\n{}\nHint: Install it using `cpanm Graph::Easy`", err_msg diff --git a/src/static_analysis/decompilation.rs b/src/static_analysis/decompilation.rs index 9048558..948026c 100644 --- a/src/static_analysis/decompilation.rs +++ b/src/static_analysis/decompilation.rs @@ -1,11 +1,66 @@ use r2pipe::R2Pipe; +use log::{info, debug, error}; /// Use Radare2's native decompiler (pdd) and return the decompiled output as a String -pub fn decompile_binary(file_path: &str) -> Result> { - let mut r2 = R2Pipe::spawn(file_path, None)?; - r2.cmd("aaa")?; // Analyze all - let decompiled_code = r2.cmd("pdd")?; // pdd = pseudo-code decompiler +pub fn decompile_binary(file_path: &str, verbose: bool) -> Result> { + info!("Starting decompilation of {}", file_path); + + // Initialize radare2 instance + let mut r2 = match R2Pipe::spawn(file_path, None) { + Ok(r2) => { + debug!("Successfully spawned r2pipe instance for decompilation"); + r2 + } + Err(e) => { + error!("Failed to spawn r2pipe: {}", e); + return Err(e.into()); + } + }; + // Configure radare2 based on verbosity + if verbose { + debug!("Configuring radare2 in verbose mode for decompilation"); + r2.cmd("e log.quiet = false")?; + } else { + debug!("Configuring radare2 in quiet mode for decompilation"); + r2.cmd("e log.quiet = true")?; + r2.cmd("e asm.quiet = true")?; + r2.cmd("e bin.relocs.apply = true")?; // Reduce warnings in quiet mode + } + + // Common configurations + r2.cmd("e scr.utf8 = false")?; + r2.cmd("e scr.interactive = false")?; + r2.cmd("e bin.cache = true")?; + debug!("Applied standard radare2 configurations"); + + // Perform analysis + info!("Performing initial analysis (aaa)"); + match r2.cmd("aaa") { + Ok(_) => debug!("Analysis completed successfully"), + Err(e) => { + error!("Analysis failed during decompilation: {}", e); + return Err(e.into()); + } + } + + // Run decompiler command + debug!("Running decompiler command: pdd"); + let decompiled_code = match r2.cmd("pdd") { + Ok(code) => { + debug!("Decompiled code retrieved ({} bytes)", code.len()); + code + } + Err(e) => { + error!("Decompiler command failed: {}", e); + return Err(e.into()); + } + }; + + // Clean up r2.close(); + debug!("r2pipe closed after decompilation"); + + info!("Decompilation completed successfully"); Ok(decompiled_code) -} +} \ No newline at end of file diff --git a/src/static_analysis/disassembly.rs b/src/static_analysis/disassembly.rs index 4579553..4f1da5c 100644 --- a/src/static_analysis/disassembly.rs +++ b/src/static_analysis/disassembly.rs @@ -1,23 +1,50 @@ use r2pipe::R2Pipe; use serde_json::Value; +use log::{info, debug, error}; -/// Disassemble the binary and return disassembly of `count` instructions as a String -pub fn disassemble_binary(file_path: &str, count: u32) -> Result> { +pub fn disassemble_binary(file_path: &str, count: u32, verbose: bool) -> Result> { + info!("Starting disassembly of {} ({} instructions)", file_path, count); + + // Initialize radare2 with appropriate verbosity let mut r2 = R2Pipe::spawn(file_path, None)?; - r2.cmd("aaa")?; // Analyze everything + + // Configure radare2 based on verbosity + if verbose { + debug!("Running radare2 in verbose mode"); + r2.cmd("e log.quiet = false")?; + r2.cmd("e asm.quiet = false")?; + } else { + debug!("Running radare2 in quiet mode"); + r2.cmd("e log.quiet = true")?; + r2.cmd("e asm.quiet = true")?; + r2.cmd("e bin.relocs.apply = true")?; // Fix the relocation warning + } + + // Common configurations + r2.cmd("e scr.utf8 = false")?; + r2.cmd("e scr.interactive = false")?; + r2.cmd("e bin.cache = true")?; + + // Perform analysis + let analysis_cmd = if verbose { "aaa" } else { "aaa" }; // Can adjust analysis level + debug!("Running analysis: {}", analysis_cmd); + r2.cmd(analysis_cmd)?; + // Generate disassembly let command = format!("pdj {}", count); + debug!("Executing command: {}", command); let json_output = r2.cmd(&command)?; + + // Parse and format output let instructions: Vec = serde_json::from_str(&json_output)?; - - let mut disassembly_output = String::new(); + let mut output = String::new(); for instr in instructions { let offset = instr["offset"].as_u64().unwrap_or(0); let mnemonic = instr["mnemonic"].as_str().unwrap_or(""); let opcode = instr["opcode"].as_str().unwrap_or(""); - disassembly_output.push_str(&format!("{:#x}:\t{}\t{}\n", offset, mnemonic, opcode)); + output.push_str(&format!("{:#x}:\t{}\t{}\n", offset, mnemonic, opcode)); } r2.close(); - Ok(disassembly_output) -} + Ok(output) +} \ No newline at end of file diff --git a/src/static_analysis/metadata.rs b/src/static_analysis/metadata.rs index a5498fc..e4a82a8 100644 --- a/src/static_analysis/metadata.rs +++ b/src/static_analysis/metadata.rs @@ -1,5 +1,6 @@ use goblin::Object; use std::fs; +use std::convert::TryInto; #[derive(Debug)] pub struct BinaryMetadata { @@ -40,9 +41,9 @@ pub fn extract_metadata(file_path: &str) -> Result { metadata.format = "PE".to_string(); - metadata.entry_point = Some(pe.entry); + metadata.entry_point = Some(pe.entry.try_into().unwrap_or(0)); metadata.sections = Some(pe.sections.len()); - metadata.image_base = Some(pe.image_base); + metadata.image_base = Some(pe.image_base.try_into().unwrap_or(0)); metadata.machine = Some(format!("{:?}", pe.header.coff_header.machine)); } Object::Mach(mach_obj) => { @@ -50,7 +51,7 @@ pub fn extract_metadata(file_path: &str) -> Result { metadata.is_64 = Some(macho.is_64); - metadata.load_commands = Some(macho.header.ncmds); + metadata.load_commands = Some(macho.header.ncmds.try_into().unwrap_or(0)); metadata.cpu_type = Some(format!("{:?}", macho.header.cputype)); } goblin::mach::Mach::Fat(fat) => { @@ -64,4 +65,4 @@ pub fn extract_metadata(file_path: &str) -> Result, verbose: bool) { + let log_level = if verbose { + LevelFilter::Debug + } else { + LevelFilter::Info + }; + + let mut loggers: Vec> = vec![ + TermLogger::new( + log_level, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto + ), + ]; + + if let Some(path) = log_file { + match File::create(path) { + Ok(file) => { + loggers.push(WriteLogger::new( + LevelFilter::Debug, + Config::default(), + file, + )); + } + Err(e) => { + eprintln!("Failed to create log file: {}", e); + } + } + } + + CombinedLogger::init(loggers).unwrap(); + + info!("Logging initialized"); + if let Some(path) = log_file { + info!("Logging to file: {:?}", path); + } +} \ No newline at end of file From db5b5611efe317f154aa744e13adc431c0070b21 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:09:45 +0530 Subject: [PATCH 11/27] fix: better tui, linting fixes.. --- .github/workflows/release.yaml | 151 +++++++++++++++ deny.toml | 239 +++++++++++++++++++++++ src/cli.rs | 280 ++++++++++++++++++--------- src/main.rs | 6 +- src/static_analysis/callgraph.rs | 11 +- src/static_analysis/decompilation.rs | 19 +- src/static_analysis/disassembly.rs | 32 +-- src/static_analysis/metadata.rs | 4 +- src/static_analysis/mod.rs | 8 +- src/utils/mod.rs | 1 + 10 files changed, 629 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100644 deny.toml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d109e65 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,151 @@ +name: Release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +jobs: + # First validate the build works properly + build_and_test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Build + run: cargo build --all-features --release + - name: Run tests + run: cargo test --all-features + + # Build for all target platforms + build_release: + name: Build Release Binaries + needs: build_and_test + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + artifact_name: rustybox + asset_name: rustybox-linux-amd64 + target: x86_64-unknown-linux-gnu + - os: ubuntu-latest + artifact_name: rustybox + asset_name: rustybox-linux-arm64 + target: aarch64-unknown-linux-gnu + - os: windows-latest + artifact_name: rustybox.exe + asset_name: rustybox-windows-amd64.exe + target: x86_64-pc-windows-msvc + - os: macos-latest + artifact_name: rustybox + asset_name: rustybox-macos-amd64 + target: x86_64-apple-darwin + - os: macos-latest + artifact_name: rustybox + asset_name: rustybox-macos-arm64 + target: aarch64-apple-darwin + + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ matrix.target }} + + # Install cross-compilation tools where needed + - name: Install cross-compilation dependencies + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + # Build the binary + - name: Build binary + run: cargo build --release --target ${{ matrix.target }} + + # Rename binary for upload + - name: Prepare binary + shell: bash + run: | + mkdir -p release + if [ "${{ matrix.os }}" = "windows-latest" ]; then + cp target/${{ matrix.target }}/release/${{ matrix.artifact_name }} release/${{ matrix.asset_name }} + else + cp target/${{ matrix.target }}/release/${{ matrix.artifact_name }} release/${{ matrix.asset_name }} + chmod +x release/${{ matrix.asset_name }} + fi + + # Upload artifacts for release job + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.asset_name }} + path: release/${{ matrix.asset_name }} + if-no-files-found: error + + # Create GitHub Release with all binaries + create_release: + name: Create GitHub Release + needs: build_release + runs-on: ubuntu-latest + permissions: + contents: write # Required for creating releases + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Download all artifacts + - name: Download all artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + # Generate release notes from git log + - name: Generate Release Notes + id: generate_notes + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [ -z "$PREVIOUS_TAG" ]; then + git log --pretty=format:"* %s (%h)" > RELEASE_NOTES.md + else + git log --pretty=format:"* %s (%h)" $PREVIOUS_TAG..HEAD > RELEASE_NOTES.md + fi + echo "RELEASE_NOTES<> $GITHUB_ENV + cat RELEASE_NOTES.md >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + # Create the release + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: | + ## Release Notes + + ${{ env.RELEASE_NOTES }} + + ## Binary Downloads + + The following binaries are available for this release: + * Linux (amd64) + * Linux (arm64) + * Windows (amd64) + * macOS (amd64) + * macOS (arm64) + draft: false + prerelease: false + files: | + artifacts/rustybox-linux-amd64/rustybox-linux-amd64 + artifacts/rustybox-linux-arm64/rustybox-linux-arm64 + artifacts/rustybox-windows-amd64.exe/rustybox-windows-amd64.exe + artifacts/rustybox-macos-amd64/rustybox-macos-amd64 + artifacts/rustybox-macos-arm64/rustybox-macos-arm64 diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..11ecbe9 --- /dev/null +++ b/deny.toml @@ -0,0 +1,239 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# Root options + +# The graph table configures how the dependency graph is constructed and thus +# which crates the checks are performed against +[graph] +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #"x86_64-unknown-linux-musl", + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = false +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +#features = [] + +# The output table provides options for how/if diagnostics are outputted +[output] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory databases are cloned/fetched into +#db-path = "$CARGO_HOME/advisory-dbs" +# The url(s) of the advisory databases to use +#db-urls = ["https://github.com/rustsec/advisory-db"] +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", + #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, + #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish + #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, +] +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "GPL-2.0", + "MPL-2.0", + "Zlib", + "Unicode-3.0", +] +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], crate = "adler32" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The package spec the clarification applies to +#crate = "ring" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ +# Each entry is a crate relative path, and the (opaque) hash of its contents +#{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, +] +# List of crates to deny +deny = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#crate = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [ + #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies + #{ crate = "ansi_term@0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# github.com organizations to allow git sources for +github = [] +# gitlab.com organizations to allow git sources for +gitlab = [] +# bitbucket.org organizations to allow git sources for +bitbucket = [] diff --git a/src/cli.rs b/src/cli.rs index 5dd9dcd..eec7c4d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,15 +3,16 @@ use colored::Colorize; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::{ + Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, List, ListItem, Paragraph, Tabs}, - Terminal, + text::{Line, Span}, + widgets::BorderType, + widgets::{Block, Borders, Paragraph, Tabs}, }; use std::{ io, @@ -21,8 +22,9 @@ use std::{ time::{Duration, Instant}, }; -// Import our static analysis functions -use crate::static_analysis::{analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata}; +use crate::static_analysis::{ + analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata, +}; const RUSTYBOX_ASCII: &str = r#" ____ _ ____ @@ -31,7 +33,7 @@ const RUSTYBOX_ASCII: &str = r#" | _ <| |_| \__ \ |_| |_| | |_) | (_) > < |_| \_\\__,_|___/\__|\__, |____/ \___/_/\_\ |___/ - Static Binary Analysis Tool + Express Binary Analysis Tool "#; enum AnalysisMode { @@ -54,6 +56,7 @@ struct App { file_path: String, instr_count: u32, verbose: bool, + scroll_position: u16, } impl App { @@ -69,6 +72,7 @@ impl App { file_path, instr_count, verbose, + scroll_position: 0, } } @@ -93,27 +97,70 @@ impl App { match self.selected_tab { 0 => match &self.analysis_results.metadata { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {}", e), + Some(Err(e)) => format!("Error: {e}"), None => "Loading metadata...".to_string(), }, 1 => match &self.analysis_results.disassembly { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {}", e), + Some(Err(e)) => format!("Error: {e}"), None => "Loading disassembly...".to_string(), }, 2 => match &self.analysis_results.decompile { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {}", e), + Some(Err(e)) => format!("Error: {e}"), None => "Loading decompiled code...".to_string(), }, 3 => match &self.analysis_results.callgraph { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {}", e), + Some(Err(e)) => format!("Error: {e}"), None => "Loading callgraph...".to_string(), }, _ => "Unknown tab".to_string(), } } + fn scroll_down(&mut self) { + let max_scroll = self.get_max_scroll(); + if self.scroll_position < max_scroll { + self.scroll_position += 1; + } + } + + fn scroll_up(&mut self) { + if self.scroll_position > 0 { + self.scroll_position -= 1; + } + } + + fn page_down(&mut self) { + let max_scroll = self.get_max_scroll(); + self.scroll_position = (self.scroll_position + 10).min(max_scroll); + } + + fn page_up(&mut self) { + self.scroll_position = self.scroll_position.saturating_sub(10); + } + + fn scroll_to_top(&mut self) { + self.scroll_position = 0; + } + + fn scroll_to_bottom(&mut self) { + self.scroll_position = self.get_max_scroll(); + } + + fn get_max_scroll(&self) -> u16 { + let content = self.get_current_result_text(); + let line_count = content.lines().count() as u16; + + let terminal_height = crossterm::terminal::size().unwrap_or((0, 24)).1; + let content_height = terminal_height.saturating_sub(6); + + if line_count <= content_height { + return 0; + } + + line_count.saturating_sub(content_height) + } } pub fn run() -> Result<(), Box> { @@ -121,13 +168,17 @@ pub fn run() -> Result<(), Box> { let file_path = matches.get_one::("FILE").unwrap(); - // Check if file exists if !Path::new(file_path).exists() { - return Err(format!("File does not exist: {}", file_path).into()); + return Err(format!("File does not exist: {file_path}").into()); } - - // If help was requested or no GUI is requested, run in standard CLI mode - if matches.get_flag("no-tui") { + let is_flag_used = matches.get_flag("no-tui") + || matches.get_flag("metadata") + || matches.get_flag("disassemble") + || matches.get_flag("decompile") + || matches.get_flag("callgraph") + || matches.contains_id("log-file"); + + if is_flag_used { return run_standard_cli(matches); } @@ -142,9 +193,8 @@ pub fn run() -> Result<(), Box> { fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box> { let file_path = matches.get_one::("FILE").unwrap(); - println!("{}", RUSTYBOX_ASCII.cyan().bold()); + println!("{}", RUSTYBOX_ASCII.truecolor(225, 95, 80).bold()); - // Determine which analyses to run let run_all = !matches.get_flag("disassemble") && !matches.get_flag("metadata") && !matches.get_flag("decompile") @@ -162,31 +212,31 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { println!("Format: {}", metadata.format); if let Some(entry) = metadata.entry_point { - println!("Entry Point: {:#x}", entry); + println!("Entry Point: {entry:#x}"); } if let Some(sections) = metadata.sections { - println!("Number of Sections: {}", sections); + println!("Number of Sections: {sections}"); } if let Some(ph) = metadata.program_headers { - println!("Program Headers: {}", ph); + println!("Program Headers: {ph}"); } if let Some(machine) = &metadata.machine { - println!("Machine Type: {}", machine); + println!("Machine Type: {machine}"); } if let Some(image_base) = metadata.image_base { - println!("Image Base: {:#x}", image_base); + println!("Image Base: {image_base:#x}"); } if let Some(is_64) = metadata.is_64 { - println!("64-bit: {}", is_64); + println!("64-bit: {is_64}"); } if let Some(load_cmds) = metadata.load_commands { - println!("Load Commands: {}", load_cmds); + println!("Load Commands: {load_cmds}"); } if let Some(cpu) = &metadata.cpu_type { - println!("CPU Type: {}", cpu); + println!("CPU Type: {cpu}"); } if let Some(arch_count) = metadata.arch_count { - println!("Architecture Count: {}", arch_count); + println!("Architecture Count: {arch_count}"); } } Err(e) => { @@ -204,8 +254,8 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box("count").copied().unwrap_or(20); match disassemble_binary(file_path, instr_count, matches.get_flag("verbose")) { - Ok(disasm) => println!("{}", disasm), - Err(e) => eprintln!("Disassembly error: {}", e), + Ok(disasm) => println!("{disasm}"), + Err(e) => eprintln!("Disassembly error: {e}"), } } @@ -217,7 +267,7 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { - println!("{}", decompiled); + println!("{decompiled}"); } Err(e) => { eprintln!("{} {}", "[-] Error decompiling binary:".red().bold(), e); @@ -233,7 +283,7 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { - println!("{}", graph); + println!("{graph}"); } Err(e) => { eprintln!("{} {}", "[-] Error generating call graph:".red().bold(), e); @@ -256,23 +306,21 @@ fn run_tui( instr_count: u32, verbose: bool, ) -> Result<(), Box> { - // Setup terminal + println!("{}", RUSTYBOX_ASCII.truecolor(225, 95, 80).bold()); + std::thread::sleep(Duration::from_secs(2)); enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup app state let mut app = App::new(file_path.clone(), instr_count, verbose); - // Channel for background worker let (tx, rx) = mpsc::channel(); let file_path_clone = file_path.clone(); let instr_count_clone = instr_count; let verbose_clone = verbose; - // Spawn worker thread that will perform analysis thread::spawn(move || { // Run all analyses in sequence // Metadata @@ -281,31 +329,31 @@ fn run_tui( let mut output = String::new(); output.push_str(&format!("Format: {}\n", metadata.format)); if let Some(entry) = metadata.entry_point { - output.push_str(&format!("Entry Point: {:#x}\n", entry)); + output.push_str(&format!("Entry Point: {entry:#x}\n")); } if let Some(sections) = metadata.sections { - output.push_str(&format!("Number of Sections: {}\n", sections)); + output.push_str(&format!("Number of Sections: {sections}\n")); } if let Some(ph) = metadata.program_headers { - output.push_str(&format!("Program Headers: {}\n", ph)); + output.push_str(&format!("Program Headers: {ph}\n")); } if let Some(machine) = &metadata.machine { - output.push_str(&format!("Machine Type: {}\n", machine)); + output.push_str(&format!("Machine Type: {machine}\n")); } if let Some(image_base) = metadata.image_base { - output.push_str(&format!("Image Base: {:#x}\n", image_base)); + output.push_str(&format!("Image Base: {image_base:#x}\n")); } if let Some(is_64) = metadata.is_64 { - output.push_str(&format!("64-bit: {}\n", is_64)); + output.push_str(&format!("64-bit: {is_64}\n")); } if let Some(load_cmds) = metadata.load_commands { - output.push_str(&format!("Load Commands: {}\n", load_cmds)); + output.push_str(&format!("Load Commands: {load_cmds}\n")); } if let Some(cpu) = &metadata.cpu_type { - output.push_str(&format!("CPU Type: {}\n", cpu)); + output.push_str(&format!("CPU Type: {cpu}\n")); } if let Some(arch_count) = metadata.arch_count { - output.push_str(&format!("Architecture Count: {}\n", arch_count)); + output.push_str(&format!("Architecture Count: {arch_count}\n")); } Ok(output) } @@ -314,10 +362,11 @@ fn run_tui( tx.send((AnalysisMode::Metadata, metadata_result)).unwrap(); // Disassembly - let disasm_result = match disassemble_binary(&file_path_clone, instr_count_clone, verbose_clone) { - Ok(disasm) => Ok(disasm), - Err(e) => Err(e.to_string()), - }; + let disasm_result = + match disassemble_binary(&file_path_clone, instr_count_clone, verbose_clone) { + Ok(disasm) => Ok(disasm), + Err(e) => Err(e.to_string()), + }; tx.send((AnalysisMode::Disassembly, disasm_result)).unwrap(); // Decompile @@ -325,14 +374,16 @@ fn run_tui( Ok(decompiled) => Ok(decompiled), Err(e) => Err(e.to_string()), }; - tx.send((AnalysisMode::Decompile, decompile_result)).unwrap(); + tx.send((AnalysisMode::Decompile, decompile_result)) + .unwrap(); // Callgraph let callgraph_result = match analyze_callgraph(&file_path_clone) { Ok(graph) => Ok(graph), Err(e) => Err(e.to_string()), }; - tx.send((AnalysisMode::Callgraph, callgraph_result)).unwrap(); + tx.send((AnalysisMode::Callgraph, callgraph_result)) + .unwrap(); }); // Main loop @@ -362,14 +413,18 @@ fn run_tui( if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('q') => break, - KeyCode::Right => app.next_tab(), - KeyCode::Left => app.previous_tab(), - KeyCode::Tab => app.next_tab(), - KeyCode::BackTab => app.previous_tab(), + KeyCode::Right | KeyCode::Tab => app.next_tab(), + KeyCode::Left | KeyCode::BackTab => app.previous_tab(), KeyCode::Char('1') => app.selected_tab = 0, KeyCode::Char('2') => app.selected_tab = 1, KeyCode::Char('3') => app.selected_tab = 2, KeyCode::Char('4') => app.selected_tab = 3, + KeyCode::Down => app.scroll_down(), + KeyCode::Up => app.scroll_up(), + KeyCode::PageDown => app.page_down(), + KeyCode::PageUp => app.page_up(), + KeyCode::Home => app.scroll_to_top(), + KeyCode::End => app.scroll_to_bottom(), _ => {} } } @@ -380,7 +435,6 @@ fn run_tui( } } - // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -393,27 +447,32 @@ fn run_tui( } fn draw_ui(f: &mut ratatui::Frame, app: &App) { - // Create main layout - let chunks = Layout::default() + let terminal_size = f.area(); + + let main_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(8), // Header with ASCII art - Constraint::Length(3), // Tabs - Constraint::Min(0), // Content - Constraint::Length(1), // Help text + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(1), ]) - .split(f.area()); + .split(terminal_size); - // Draw ASCII Art header - let title_text = Text::from(vec![Line::from(vec![ - Span::styled(RUSTYBOX_ASCII, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), - ])]); + let top_bar = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) + .split(main_layout[0]); - let header = Paragraph::new(title_text) + let file_info = Paragraph::new(format!(" {}", app.file_path)) .style(Style::default().fg(Color::Cyan)) - .block(Block::default()); + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Target"), + ); - f.render_widget(header, chunks[0]); + f.render_widget(file_info, top_bar[0]); // Draw tabs let titles = app.tab_titles(); @@ -423,7 +482,11 @@ fn draw_ui(f: &mut ratatui::Frame, app: &App) { .collect(); let tabs = Tabs::new(tab_titles) - .block(Block::default().borders(Borders::ALL).title("Analysis Modes")) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) .select(app.selected_tab) .style(Style::default().fg(Color::White)) .highlight_style( @@ -432,28 +495,71 @@ fn draw_ui(f: &mut ratatui::Frame, app: &App) { .add_modifier(Modifier::BOLD), ); - f.render_widget(tabs, chunks[1]); + f.render_widget(tabs, top_bar[1]); - // Get content based on selected tab + // Content area let content_text = app.get_current_result_text(); + + let content_title = match app.selected_tab { + 0 => "Binary Metadata", + 1 => "Disassembly", + 2 => "Decompiled Code", + 3 => "Function Call Graph", + _ => "Unknown", + }; + + let scroll_info = if app.get_max_scroll() > 0 { + format!(" [{}/{}]", app.scroll_position, app.get_max_scroll()) + } else { + String::new() + }; + let content = Paragraph::new(content_text) - .block(Block::default().borders(Borders::ALL).title(match app.selected_tab { - 0 => "Binary Metadata", - 1 => "Disassembly", - 2 => "Decompiled Code", - 3 => "Function Call Graph", - _ => "Unknown", - })) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(format!("{content_title}{scroll_info}")), + ) .style(Style::default().fg(Color::White)) - .wrap(ratatui::widgets::Wrap { trim: true }); - - f.render_widget(content, chunks[2]); + .wrap(ratatui::widgets::Wrap { trim: true }) + .scroll((app.scroll_position, 0)); + + f.render_widget(content, main_layout[1]); + + // Help bar at bottom + let status = match app.selected_tab { + 0 => "Metadata โ–ถ", + 1 => "Disassembly โ–ถ", + 2 => "Decompile โ–ถ", + 3 => "Callgraph โ–ถ", + _ => "Unknown", + }; + + let help_text = Line::from(vec![ + Span::styled( + " q ", + Style::default() + .bg(Color::Red) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" quit | "), + Span::styled(" โ†โ†’ ", Style::default().bg(Color::Blue).fg(Color::White)), + Span::raw(" change view | "), + Span::styled(" โ†‘โ†“ ", Style::default().bg(Color::Blue).fg(Color::White)), + Span::raw(" scroll | "), + Span::styled( + status, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ]); - // Help text - let help_text = Paragraph::new("Press Tab/Left/Right to switch tabs | 1-4 to select tab | q to quit") - .style(Style::default().fg(Color::DarkGray)); + let help_bar = Paragraph::new(help_text).style(Style::default().bg(Color::DarkGray)); - f.render_widget(help_text, chunks[3]); + f.render_widget(help_bar, main_layout[2]); } fn create_cli() -> Command { @@ -522,4 +628,4 @@ fn create_cli() -> Command { .help("Run in classic command-line mode without TUI") .action(clap::ArgAction::SetTrue), ) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index e011e72..b8f9073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ -mod static_analysis; mod cli; +mod static_analysis; mod utils; fn main() { if let Err(e) = cli::run() { - eprintln!("Error: {}", e); + eprintln!("Error: {e}"); std::process::exit(1); } -} \ No newline at end of file +} diff --git a/src/static_analysis/callgraph.rs b/src/static_analysis/callgraph.rs index d0c8b2a..e318531 100644 --- a/src/static_analysis/callgraph.rs +++ b/src/static_analysis/callgraph.rs @@ -1,14 +1,16 @@ use std::fs; use std::process::{Command, Stdio}; -use std::path::Path; -use std::io::Write; pub fn analyze_callgraph(binary_path: &str) -> Result> { let dot_path = "callgraph.dot"; // Step 1: Run radare2 and capture DOT callgraph output let output = Command::new("r2") - .args(["-Aqc", "e bin.relocs.apply=true; e bin.cache=true; aa; agfd", binary_path]) + .args([ + "-Aqc", + "e bin.relocs.apply=true; e bin.cache=true; aa; agfd", + binary_path, + ]) .stdout(Stdio::piped()) .output()?; @@ -34,8 +36,7 @@ pub fn analyze_callgraph(binary_path: &str) -> Result Result> { - info!("Starting decompilation of {}", file_path); - +pub fn decompile_binary( + file_path: &str, + verbose: bool, +) -> Result> { + info!("Starting decompilation of {file_path}"); + // Initialize radare2 instance let mut r2 = match R2Pipe::spawn(file_path, None) { Ok(r2) => { @@ -12,7 +15,7 @@ pub fn decompile_binary(file_path: &str, verbose: bool) -> Result { - error!("Failed to spawn r2pipe: {}", e); + error!("Failed to spawn r2pipe: {e}"); return Err(e.into()); } }; @@ -39,7 +42,7 @@ pub fn decompile_binary(file_path: &str, verbose: bool) -> Result debug!("Analysis completed successfully"), Err(e) => { - error!("Analysis failed during decompilation: {}", e); + error!("Analysis failed during decompilation: {e}"); return Err(e.into()); } } @@ -52,7 +55,7 @@ pub fn decompile_binary(file_path: &str, verbose: bool) -> Result { - error!("Decompiler command failed: {}", e); + error!("Decompiler command failed: {e}"); return Err(e.into()); } }; @@ -63,4 +66,4 @@ pub fn decompile_binary(file_path: &str, verbose: bool) -> Result Result> { - info!("Starting disassembly of {} ({} instructions)", file_path, count); - +pub fn disassemble_binary( + file_path: &str, + count: u32, + verbose: bool, +) -> Result> { + info!( + "Starting disassembly of {file_path} ({count} instructions)" + ); + // Initialize radare2 with appropriate verbosity let mut r2 = R2Pipe::spawn(file_path, None)?; - + // Configure radare2 based on verbosity if verbose { debug!("Running radare2 in verbose mode"); @@ -17,9 +23,9 @@ pub fn disassemble_binary(file_path: &str, count: u32, verbose: bool) -> Result< debug!("Running radare2 in quiet mode"); r2.cmd("e log.quiet = true")?; r2.cmd("e asm.quiet = true")?; - r2.cmd("e bin.relocs.apply = true")?; // Fix the relocation warning + r2.cmd("e bin.relocs.apply = true")?; // Fix the relocation warning } - + // Common configurations r2.cmd("e scr.utf8 = false")?; r2.cmd("e scr.interactive = false")?; @@ -27,14 +33,14 @@ pub fn disassemble_binary(file_path: &str, count: u32, verbose: bool) -> Result< // Perform analysis let analysis_cmd = if verbose { "aaa" } else { "aaa" }; // Can adjust analysis level - debug!("Running analysis: {}", analysis_cmd); + debug!("Running analysis: {analysis_cmd}"); r2.cmd(analysis_cmd)?; // Generate disassembly - let command = format!("pdj {}", count); - debug!("Executing command: {}", command); + let command = format!("pdj {count}"); + debug!("Executing command: {command}"); let json_output = r2.cmd(&command)?; - + // Parse and format output let instructions: Vec = serde_json::from_str(&json_output)?; let mut output = String::new(); @@ -42,9 +48,9 @@ pub fn disassemble_binary(file_path: &str, count: u32, verbose: bool) -> Result< let offset = instr["offset"].as_u64().unwrap_or(0); let mnemonic = instr["mnemonic"].as_str().unwrap_or(""); let opcode = instr["opcode"].as_str().unwrap_or(""); - output.push_str(&format!("{:#x}:\t{}\t{}\n", offset, mnemonic, opcode)); + output.push_str(&format!("{offset:#x}:\t{mnemonic}\t{opcode}\n")); } r2.close(); Ok(output) -} \ No newline at end of file +} diff --git a/src/static_analysis/metadata.rs b/src/static_analysis/metadata.rs index e4a82a8..a96195f 100644 --- a/src/static_analysis/metadata.rs +++ b/src/static_analysis/metadata.rs @@ -1,6 +1,6 @@ use goblin::Object; -use std::fs; use std::convert::TryInto; +use std::fs; #[derive(Debug)] pub struct BinaryMetadata { @@ -65,4 +65,4 @@ pub fn extract_metadata(file_path: &str) -> Result Date: Sat, 10 May 2025 03:11:09 +0530 Subject: [PATCH 12/27] fix: linting -v2 --- src/static_analysis/callgraph.rs | 6 +++--- src/static_analysis/disassembly.rs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/static_analysis/callgraph.rs b/src/static_analysis/callgraph.rs index e318531..5defd20 100644 --- a/src/static_analysis/callgraph.rs +++ b/src/static_analysis/callgraph.rs @@ -35,9 +35,9 @@ pub fn analyze_callgraph(binary_path: &str) -> Result Result> { - info!( - "Starting disassembly of {file_path} ({count} instructions)" - ); + info!("Starting disassembly of {file_path} ({count} instructions)"); // Initialize radare2 with appropriate verbosity let mut r2 = R2Pipe::spawn(file_path, None)?; From d0941361c5d0d02c4f1fc30dd23f802d161492ff Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:20:04 +0530 Subject: [PATCH 13/27] fix: minor bugs --- src/cli.rs | 61 +++++++++++++++--------------- src/static_analysis/disassembly.rs | 2 +- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index eec7c4d..89f336c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,9 +10,9 @@ use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, - text::{Line, Span}, + text::{Line, Span, Text}, widgets::BorderType, - widgets::{Block, Borders, Paragraph, Tabs}, + widgets::{Block, Borders, List, ListItem, Paragraph, Tabs}, }; use std::{ io, @@ -50,6 +50,7 @@ struct AnalysisResult { callgraph: Option>, } +#[allow(dead_code)] struct App { selected_tab: usize, analysis_results: AnalysisResult, @@ -97,22 +98,22 @@ impl App { match self.selected_tab { 0 => match &self.analysis_results.metadata { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {e}"), + Some(Err(e)) => format!("Error: {}", e), None => "Loading metadata...".to_string(), }, 1 => match &self.analysis_results.disassembly { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {e}"), + Some(Err(e)) => format!("Error: {}", e), None => "Loading disassembly...".to_string(), }, 2 => match &self.analysis_results.decompile { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {e}"), + Some(Err(e)) => format!("Error: {}", e), None => "Loading decompiled code...".to_string(), }, 3 => match &self.analysis_results.callgraph { Some(Ok(text)) => text.clone(), - Some(Err(e)) => format!("Error: {e}"), + Some(Err(e)) => format!("Error: {}", e), None => "Loading callgraph...".to_string(), }, _ => "Unknown tab".to_string(), @@ -169,7 +170,7 @@ pub fn run() -> Result<(), Box> { let file_path = matches.get_one::("FILE").unwrap(); if !Path::new(file_path).exists() { - return Err(format!("File does not exist: {file_path}").into()); + return Err(format!("File does not exist: {}", file_path).into()); } let is_flag_used = matches.get_flag("no-tui") || matches.get_flag("metadata") @@ -212,31 +213,31 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { println!("Format: {}", metadata.format); if let Some(entry) = metadata.entry_point { - println!("Entry Point: {entry:#x}"); + println!("Entry Point: {:#x}", entry); } if let Some(sections) = metadata.sections { - println!("Number of Sections: {sections}"); + println!("Number of Sections: {}", sections); } if let Some(ph) = metadata.program_headers { - println!("Program Headers: {ph}"); + println!("Program Headers: {}", ph); } if let Some(machine) = &metadata.machine { - println!("Machine Type: {machine}"); + println!("Machine Type: {}", machine); } if let Some(image_base) = metadata.image_base { - println!("Image Base: {image_base:#x}"); + println!("Image Base: {:#x}", image_base); } if let Some(is_64) = metadata.is_64 { - println!("64-bit: {is_64}"); + println!("64-bit: {}", is_64); } if let Some(load_cmds) = metadata.load_commands { - println!("Load Commands: {load_cmds}"); + println!("Load Commands: {}", load_cmds); } if let Some(cpu) = &metadata.cpu_type { - println!("CPU Type: {cpu}"); + println!("CPU Type: {}", cpu); } if let Some(arch_count) = metadata.arch_count { - println!("Architecture Count: {arch_count}"); + println!("Architecture Count: {}", arch_count); } } Err(e) => { @@ -254,8 +255,8 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box("count").copied().unwrap_or(20); match disassemble_binary(file_path, instr_count, matches.get_flag("verbose")) { - Ok(disasm) => println!("{disasm}"), - Err(e) => eprintln!("Disassembly error: {e}"), + Ok(disasm) => println!("{}", disasm), + Err(e) => eprintln!("Disassembly error: {}", e), } } @@ -267,7 +268,7 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { - println!("{decompiled}"); + println!("{}", decompiled); } Err(e) => { eprintln!("{} {}", "[-] Error decompiling binary:".red().bold(), e); @@ -283,7 +284,7 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box { - println!("{graph}"); + println!("{}", graph); } Err(e) => { eprintln!("{} {}", "[-] Error generating call graph:".red().bold(), e); @@ -329,31 +330,31 @@ fn run_tui( let mut output = String::new(); output.push_str(&format!("Format: {}\n", metadata.format)); if let Some(entry) = metadata.entry_point { - output.push_str(&format!("Entry Point: {entry:#x}\n")); + output.push_str(&format!("Entry Point: {:#x}\n", entry)); } if let Some(sections) = metadata.sections { - output.push_str(&format!("Number of Sections: {sections}\n")); + output.push_str(&format!("Number of Sections: {}\n", sections)); } if let Some(ph) = metadata.program_headers { - output.push_str(&format!("Program Headers: {ph}\n")); + output.push_str(&format!("Program Headers: {}\n", ph)); } if let Some(machine) = &metadata.machine { - output.push_str(&format!("Machine Type: {machine}\n")); + output.push_str(&format!("Machine Type: {}\n", machine)); } if let Some(image_base) = metadata.image_base { - output.push_str(&format!("Image Base: {image_base:#x}\n")); + output.push_str(&format!("Image Base: {:#x}\n", image_base)); } if let Some(is_64) = metadata.is_64 { - output.push_str(&format!("64-bit: {is_64}\n")); + output.push_str(&format!("64-bit: {}\n", is_64)); } if let Some(load_cmds) = metadata.load_commands { - output.push_str(&format!("Load Commands: {load_cmds}\n")); + output.push_str(&format!("Load Commands: {}\n", load_cmds)); } if let Some(cpu) = &metadata.cpu_type { - output.push_str(&format!("CPU Type: {cpu}\n")); + output.push_str(&format!("CPU Type: {}\n", cpu)); } if let Some(arch_count) = metadata.arch_count { - output.push_str(&format!("Architecture Count: {arch_count}\n")); + output.push_str(&format!("Architecture Count: {}\n", arch_count)); } Ok(output) } @@ -519,7 +520,7 @@ fn draw_ui(f: &mut ratatui::Frame, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title(format!("{content_title}{scroll_info}")), + .title(format!("{}{}", content_title, scroll_info)), ) .style(Style::default().fg(Color::White)) .wrap(ratatui::widgets::Wrap { trim: true }) diff --git a/src/static_analysis/disassembly.rs b/src/static_analysis/disassembly.rs index 895a095..d836f61 100644 --- a/src/static_analysis/disassembly.rs +++ b/src/static_analysis/disassembly.rs @@ -30,7 +30,7 @@ pub fn disassemble_binary( r2.cmd("e bin.cache = true")?; // Perform analysis - let analysis_cmd = if verbose { "aaa" } else { "aaa" }; // Can adjust analysis level + let analysis_cmd = { "aaa" }; debug!("Running analysis: {analysis_cmd}"); r2.cmd(analysis_cmd)?; From 5acd1ede47399af7d775240c1196b55063ef37c2 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:26:02 +0530 Subject: [PATCH 14/27] chore: update release.yaml --- .../workflows/{release.yaml => release.yml} | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) rename .github/workflows/{release.yaml => release.yml} (85%) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yml similarity index 85% rename from .github/workflows/release.yaml rename to .github/workflows/release.yml index d109e65..e38ea8d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yml @@ -4,9 +4,9 @@ on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" + - "dev-v[0-9]+.[0-9]+.[0-9]+" jobs: - # First validate the build works properly build_and_test: name: Build and Test runs-on: ubuntu-latest @@ -20,7 +20,6 @@ jobs: - name: Run tests run: cargo test --all-features - # Build for all target platforms build_release: name: Build Release Binaries needs: build_and_test @@ -56,18 +55,15 @@ jobs: toolchain: stable targets: ${{ matrix.target }} - # Install cross-compilation tools where needed - name: Install cross-compilation dependencies if: matrix.target == 'aarch64-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu - # Build the binary - name: Build binary run: cargo build --release --target ${{ matrix.target }} - # Rename binary for upload - name: Prepare binary shell: bash run: | @@ -79,7 +75,6 @@ jobs: chmod +x release/${{ matrix.asset_name }} fi - # Upload artifacts for release job - name: Upload artifacts uses: actions/upload-artifact@v3 with: @@ -87,25 +82,22 @@ jobs: path: release/${{ matrix.asset_name }} if-no-files-found: error - # Create GitHub Release with all binaries create_release: name: Create GitHub Release needs: build_release runs-on: ubuntu-latest permissions: - contents: write # Required for creating releases + contents: write steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - # Download all artifacts - name: Download all artifacts uses: actions/download-artifact@v3 with: path: artifacts - # Generate release notes from git log - name: Generate Release Notes id: generate_notes run: | @@ -119,7 +111,15 @@ jobs: cat RELEASE_NOTES.md >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - # Create the release + - name: Check if development release + id: check_dev + run: | + if [[ "${{ github.ref_name }}" == dev-* ]]; then + echo "is_dev=true" >> $GITHUB_OUTPUT + else + echo "is_dev=false" >> $GITHUB_OUTPUT + fi + - name: Create Release id: create_release uses: softprops/action-gh-release@v1 @@ -127,7 +127,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref_name }} - name: Release ${{ github.ref_name }} + name: ${{ steps.check_dev.outputs.is_dev == 'true' && format('Development Release {0}', github.ref_name) || format('Release {0}', github.ref_name) }} body: | ## Release Notes @@ -141,8 +141,10 @@ jobs: * Windows (amd64) * macOS (amd64) * macOS (arm64) + + ${{ steps.check_dev.outputs.is_dev == 'true' && 'โš ๏ธ This is a development release from the develop branch and may contain unstable features.' || '' }} draft: false - prerelease: false + prerelease: ${{ steps.check_dev.outputs.is_dev }} files: | artifacts/rustybox-linux-amd64/rustybox-linux-amd64 artifacts/rustybox-linux-arm64/rustybox-linux-arm64 From ebc8f682fa18b0a0a4274201b9688050318bd57d Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:30:49 +0530 Subject: [PATCH 15/27] chore: update release.yml -v2 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e38ea8d..04f3047 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,7 @@ jobs: fi - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.asset_name }} path: release/${{ matrix.asset_name }} From 1b6bf8063550950aebc4c8de25b5d4b81f98f82a Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:34:50 +0530 Subject: [PATCH 16/27] chore: update release.yml -v3 --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04f3047..6b64d53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,8 +3,8 @@ name: Release on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "dev-v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+*" + - "dev-v[0-9]+.[0-9]+.[0-9]+*" jobs: build_and_test: From 2121ac96c315ecea07d6fbb8f48a2bb520f6746c Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:41:18 +0530 Subject: [PATCH 17/27] chore: update release.yml -v4 --- .github/workflows/release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b64d53..543bc2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,10 +31,6 @@ jobs: artifact_name: rustybox asset_name: rustybox-linux-amd64 target: x86_64-unknown-linux-gnu - - os: ubuntu-latest - artifact_name: rustybox - asset_name: rustybox-linux-arm64 - target: aarch64-unknown-linux-gnu - os: windows-latest artifact_name: rustybox.exe asset_name: rustybox-windows-amd64.exe @@ -137,7 +133,6 @@ jobs: The following binaries are available for this release: * Linux (amd64) - * Linux (arm64) * Windows (amd64) * macOS (amd64) * macOS (arm64) @@ -147,7 +142,6 @@ jobs: prerelease: ${{ steps.check_dev.outputs.is_dev }} files: | artifacts/rustybox-linux-amd64/rustybox-linux-amd64 - artifacts/rustybox-linux-arm64/rustybox-linux-arm64 artifacts/rustybox-windows-amd64.exe/rustybox-windows-amd64.exe artifacts/rustybox-macos-amd64/rustybox-macos-amd64 artifacts/rustybox-macos-arm64/rustybox-macos-arm64 From b93e42c15c8feae85cf061ca2abf3357f58404f0 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:48:32 +0530 Subject: [PATCH 18/27] chore: update release.yml, rm windows rls(temp) --- .github/workflows/release.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 543bc2c..179eaae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,10 +31,10 @@ jobs: artifact_name: rustybox asset_name: rustybox-linux-amd64 target: x86_64-unknown-linux-gnu - - os: windows-latest - artifact_name: rustybox.exe - asset_name: rustybox-windows-amd64.exe - target: x86_64-pc-windows-msvc + # - os: windows-latest + # artifact_name: rustybox.exe + # asset_name: rustybox-windows-amd64.exe + # target: x86_64-pc-windows-msvc - os: macos-latest artifact_name: rustybox asset_name: rustybox-macos-amd64 @@ -142,6 +142,5 @@ jobs: prerelease: ${{ steps.check_dev.outputs.is_dev }} files: | artifacts/rustybox-linux-amd64/rustybox-linux-amd64 - artifacts/rustybox-windows-amd64.exe/rustybox-windows-amd64.exe artifacts/rustybox-macos-amd64/rustybox-macos-amd64 artifacts/rustybox-macos-arm64/rustybox-macos-arm64 From 12170153f7582bb4e37a4e571351ceaa1ad15c46 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 03:54:59 +0530 Subject: [PATCH 19/27] fix: outdated action --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 179eaae..1c7b878 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,7 @@ jobs: fetch-depth: 0 - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts From 2d2008bd6a37e4e7826ed0450b91b91a397c61f9 Mon Sep 17 00:00:00 2001 From: hsanjay <1ds22cy017@dsce.edu.in> Date: Sat, 10 May 2025 07:04:49 +0530 Subject: [PATCH 20/27] added binary parsing feature and added the cli command for it --- Cargo.toml | 6 + src/cli.rs | 18 +- src/static_analysis/binary_parsing.rs | 854 ++++++++++++++++++++++++++ src/static_analysis/mod.rs | 4 + 4 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 src/static_analysis/binary_parsing.rs diff --git a/Cargo.toml b/Cargo.toml index c97e711..803051f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,12 @@ ascii_tree = "0.1.1" clap = "4.5.37" colored = "3.0.0" goblin = "0.9.3" +pelite = "0.9" +pe-parser = "0.3" +lief = { git = "https://github.com/lief-project/LIEF", branch = "main" } +gimli = "0.26" +owo-colors = "3.5" +prettytable = "0.10" log = "0.4.27" logging = "0.1.0" r2pipe = "0.7.0" diff --git a/src/cli.rs b/src/cli.rs index 89f336c..9ed748a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,7 +23,7 @@ use std::{ }; use crate::static_analysis::{ - analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata, + analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata, analyze_binary, }; const RUSTYBOX_ASCII: &str = r#" @@ -196,6 +196,15 @@ fn run_standard_cli(matches: clap::ArgMatches) -> Result<(), Box Command { .help("Run in classic command-line mode without TUI") .action(clap::ArgAction::SetTrue), ) + + .arg( + Arg::new("binaryp") + .long("binaryp") + .help("Analyze the binary using the binaryp command") + .action(clap::ArgAction::SetTrue), + ) } diff --git a/src/static_analysis/binary_parsing.rs b/src/static_analysis/binary_parsing.rs new file mode 100644 index 0000000..6ab9f9c --- /dev/null +++ b/src/static_analysis/binary_parsing.rs @@ -0,0 +1,854 @@ +#![allow(dead_code)] +#![allow(unused_imports)] + + +use goblin::{ + elf::{Elf,dynamic::*}, + mach::{Mach}, + pe::PE, + Object, +}; +use std::fs::File; +use std::io::Read; +use std::{fs, error::Error}; +use std::path::Path; +use gimli::{ + DebugAbbrev, DebugInfo, DebugLine, DebugStr, EndianSlice, LittleEndian, + DwTag +}; + +use owo_colors::OwoColorize; +use prettytable::{Table, Row, Cell}; + + + + +pub fn analyze_binary(path: &str) -> Result<(), Box> { + let buffer = fs::read(path)?; + let file_size = buffer.len() as u64; + + match Object::parse(&buffer)? { + Object::PE(pe) => analyze_pe(&pe, &buffer, file_size), + Object::Elf(elf) => analyze_elf(&elf, &buffer), + _ => println!("Unsupported or unrecognized binary format"), + } + + Ok(()) +} + +fn analyze_pe(pe: &PE, buffer: &[u8], file_size: u64) { + println!("\n\n\n\n{:^120}","+++++++++++++++๐Ÿ” Detected PE (Windows) format+++++++++++++++".green().bold()); + println!("\n\n{} 0x{:x}","๐Ÿš€ Entry Point: ".red().bold(), pe.entry); + + analyze_sections(pe, buffer); + analyze_imports(pe); + analyze_exports(pe); + analyze_tls(pe); + analyze_debug(pe); + analyze_cert_table(pe); + detect_overlay(pe, file_size); + check_anti_debug(pe, buffer); + calculate_entropy(buffer); + + // Try parsing the same buffer as ELF + if let Ok(elf) = Elf::parse(buffer) { + println!("\n--- Trying ELF analysis ---"); + analyze_elf(&elf, &buffer); + } else { + println!("โŒ Not a valid ELF file."); + } +} + +//these are PE Binary Parsing Techniques + +fn analyze_sections(pe: &PE, buffer: &[u8]) { + println!("{}","\n\n๐Ÿ“ฆ Sections:".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Name"), + Cell::new("Virtual Address"), + Cell::new("Virtual Size"), + Cell::new("Permissions"), + Cell::new("Entropy"), + ])); + + + + for section in &pe.sections { + let name = String::from_utf8_lossy(§ion.name).trim().to_string(); + let va = section.virtual_address; + let size = section.virtual_size; + let perms = format!( + "{}{}{}", + if section.characteristics & 0x20000000 != 0 { "X" } else { "-" }, + if section.characteristics & 0x80000000 != 0 { "W" } else { "-" }, + if section.characteristics & 0x40000000 != 0 { "R" } else { "-" } + ); + + let start = section.pointer_to_raw_data as usize; + let end = start + section.size_of_raw_data as usize; + let data = if end <= buffer.len() { &buffer[start..end] } else { &[] }; + let entropy = calculate_entropy(data); + + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new(&format!("0x{:08x}", va)), + Cell::new(&format!("0x{:08x}", size)), + Cell::new(&perms), + Cell::new(&format!("{:.2}", entropy)), + ])); + } + table.printstd(); +} + +fn analyze_imports(pe: &PE) { + println!("{}","\n\n๐Ÿ”— Imported DLLs & Functions :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("DLL"), + Cell::new("Function"), + Cell::new("Ordinal"), + ])); + + + for import in &pe.imports { + table.add_row(Row::new(vec![ + Cell::new(&import.dll), + Cell::new(&import.name), + Cell::new(&format!("{}", import.ordinal)), + ])); + } + table.printstd(); +} + +fn analyze_exports(pe: &PE) { + println!("{}","\n\n๐Ÿ“ค Exported Symbols :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Function"), + Cell::new("RVA"), + ])); + + for export in &pe.exports { + let name = export.name.unwrap_or(""); + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("0x{:x}", export.rva)), + ])); + } + table.printstd(); +} + + + +fn analyze_tls(pe: &PE) { + println!("{}","\n\n๐Ÿงต TLS Callback RVA :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Virtual Address"), + Cell::new("Size"), + ])); + + if let Some(optional_header) = &pe.header.optional_header { + if let Some(tls_dir) = optional_header.data_directories.get_tls_table() { + table.add_row(Row::new(vec![ + Cell::new(&format!("0x{:x}", tls_dir.virtual_address)), + Cell::new(&format!("{}", tls_dir.size)), + ])); + } else { + println!(" - No TLS table."); + } + } + table.printstd(); +} + + + +fn analyze_debug(pe: &PE) { + println!("{}","\n\n๐Ÿž Debug Directory :".red().bold()); + if let Some(debug_data) = &pe.debug_data { + if let Some(pdb_info) = &debug_data.codeview_pdb70_debug_info { + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Field"), + Cell::new("Value"), + ])); + table.add_row(Row::new(vec![ + Cell::new("PDB Signature"), + Cell::new(&format!("{:?}", pdb_info.signature)), + ])); + table.add_row(Row::new(vec![ + Cell::new("PDB Age"), + Cell::new(&pdb_info.age.to_string()), + ])); + table.add_row(Row::new(vec![ + Cell::new("PDB Path"), + Cell::new(&String::from_utf8_lossy(&pdb_info.filename)), + ])); + + table.printstd(); + + } else { + println!(" - No CodeView PDB 7.0 debug info."); + } + } else { + println!(" - No debug data found."); + } +} + +fn analyze_cert_table(pe: &PE) { + println!("{}","\n\n๐Ÿ“œ Certificate Table :".red().bold()); + if let Some(optional_header) = &pe.header.optional_header { + if let Some(cert_dir) = optional_header.data_directories.get_certificate_table() { + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Field"), + Cell::new("Value"), + ])); + table.add_row(Row::new(vec![ + Cell::new("Virtual Address"), + Cell::new(&format!("0x{:x}", cert_dir.virtual_address)), + ])); + table.add_row(Row::new(vec![ + Cell::new("Size"), + Cell::new(&cert_dir.size.to_string()), + ])); + + table.printstd(); + } else { + println!(" - No certificate table found."); + } + } +} + + + +fn detect_overlay(pe: &PE, file_size: u64) { + + + + let last_section_end = pe.sections.iter() + .map(|s| s.pointer_to_raw_data as u64 + s.size_of_raw_data as u64) + .max() + .unwrap_or(0); + + if last_section_end < file_size { + println!("{}", "\n\nโš ๏ธ Overlay Detected :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Field"), + Cell::new("Value"), + ])); + table.add_row(Row::new(vec![ + Cell::new("Hidden Data Size"), + Cell::new(&format!("{} bytes", file_size - last_section_end)), + ])); + + table.printstd(); + } +} + + + +fn check_anti_debug(_pe: &PE, buffer: &[u8]) { + println!("{}","\n\n๐Ÿ•ต๏ธ Anti-Debugging Techniques :".red().bold()); + let suspicious_bytes: &[&[u8]] = &[ + &[0x64, 0xA1, 0x30, 0x00, 0x00, 0x00], // FS:[30h] โ€” PEB access + &[0xCC], // INT 3 (breakpoint) + ]; + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Signature").style_spec("Fb"), // Fb = Bold font + Cell::new("Detected").style_spec("Fb"), + ])); + + let flags = 0; + + let mut found_any = false; + + for sig in suspicious_bytes { + let found = buffer.windows(sig.len()).any(|w| w == *sig); + table.add_row(Row::new(vec![ + Cell::new(&format!("{:?}", sig)), + Cell::new(if found { + found_any = true; + "โœ”๏ธ" + } else { + "โŒ" + }), + ])); + } + table.printstd(); + + if flags == 0 { + println!(" - No obvious anti-debugging signs."); + } +} + + + +fn calculate_entropy(data: &[u8]) -> f64 { + if data.is_empty() { + return 0.0; + } + + let mut freq = [0usize; 256]; + for &b in data { + freq[b as usize] += 1; + } + + let len = data.len() as f64; + let mut entropy = 0.0; + + for &count in &freq { + if count == 0 { continue; } + let p = count as f64 / len; + entropy += -p * p.log2(); + } + entropy +} + + + +fn analyze_elf(elf: &Elf, bytes: &[u8]) { + println!("\n\n\n\n{:^120}","+++++++++++++++๐Ÿ” Analyzing ELF Binary +++++++++++++++".green().bold()); + // Entry point address + println!("\n\n{} 0x{:x}","๐Ÿš€ Entry Point: ".red().bold(), elf.entry); + analyze_elf_imports(elf); + analyze_elf_exports(elf); + parse_elf_header(bytes); + parse_program_headers(bytes); + parse_section_headers(bytes); + parse_symbol_tables(bytes); + parse_string_tables(bytes); + parse_relocations(bytes); + parse_dynamic_section(bytes); + parse_hash_tables(bytes); + parse_notes(bytes); + parse_versioning_sections(bytes); + parse_and_print_dwarf_functions(bytes).unwrap_or_else(|_| { + println!("Failed to parse DWARF functions."); + }); +} + + +//these are ELF Binary Parsing Techniques + + +type Endian = LittleEndian; +type ReaderType<'a> = EndianSlice<'a, Endian>; + +// Helper to extract section contents +fn get_section<'a>( + bytes: &'a [u8], + elf: &Elf, + section_name: &str, +) -> Result<&'a [u8], Box> { + for header in &elf.section_headers { + if let Some(name) = elf.shdr_strtab.get_at(header.sh_name) { + if name == section_name { + let start = header.sh_offset as usize; + let end = start + header.sh_size as usize; + return Ok(&bytes[start..end]); + } + } + } + Err(format!("Section {} not found", section_name).into()) +} + + + +fn analyze_elf_imports(elf: &Elf) { + println!("{}","\n\n๐Ÿ”— Imported Shared Libraries:".red().bold()); + + if let Some(dynamic) = &elf.dynamic { + for dyn_ in &dynamic.dyns { + if dyn_.d_tag == goblin::elf::dynamic::DT_NEEDED { + if let Some(name) = elf.dynstrtab.get_at(dyn_.d_val as usize) { + println!(" - {}", name); + } + } + } + } +} + + + + +fn analyze_elf_exports(elf: &Elf) { + println!("{}","\n\n๐Ÿ“ค Exported Symbols:".red().bold()); + + let mut table = Table::new(); + + // Add the headers + table.add_row(Row::new(vec![ + Cell::new("Name"), + Cell::new("Address"), + Cell::new("Type"), + Cell::new("Bind"), + ])); + + for sym in &elf.dynsyms { + let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or(""); + let bind = sym.st_bind(); + let typ = sym.st_type(); + + // Typically exported: global binding & function/object type + if bind == goblin::elf::sym::STB_GLOBAL && (typ == goblin::elf::sym::STT_FUNC || typ == goblin::elf::sym::STT_OBJECT) { + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("0x{:x}", sym.st_value)), + Cell::new(&format!("{:?}", typ)), + Cell::new(&format!("{:?}", bind)), + ])); + } + } + // Print the table + table.printstd(); +} + + + +fn parse_elf_header(bytes: &[u8]) { + + + let mut table = Table::new(); + + // Add headers to the table + table.add_row(Row::new(vec![ + Cell::new("Type"), + Cell::new("Machine"), + Cell::new("Entry point"), + Cell::new("Program header offset"), + Cell::new("Section header offset"), + ])); + + + if let Ok(elf) = Elf::parse(bytes) { + table.add_row(Row::new(vec![ + Cell::new(&format!("{:?}", elf.header.e_type)), + Cell::new(&format!("{:?}", elf.header.e_machine)), + Cell::new(&format!("0x{:x}", elf.entry)), + Cell::new(&format!("{}", elf.header.e_phoff)), + Cell::new(&format!("{}", elf.header.e_shoff)), + ])); + } + println!("{}","\n\nโš™๏ธ ELF Header :".red().bold()); + // Print the table + table.printstd(); +} + +fn parse_program_headers(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\nโš™๏ธ Program Headers :".red().bold()); + + let mut table = Table::new(); + + // Add headers to the table + table.add_row(Row::new(vec![ + Cell::new("Type"), + Cell::new("Offset"), + Cell::new("VAddr"), + Cell::new("PAddr"), + Cell::new("Filesz"), + Cell::new("Memsz"), + Cell::new("Flags"), + ])); + + for ph in &elf.program_headers { + + table.add_row(Row::new(vec![ + Cell::new(&format!("{:?}", ph.p_type)), + Cell::new(&format!("{}", ph.p_offset)), + Cell::new(&format!("0x{:x}", ph.p_vaddr)), + Cell::new(&format!("0x{:x}", ph.p_paddr)), + Cell::new(&format!("{}", ph.p_filesz)), + Cell::new(&format!("{}", ph.p_memsz)), + Cell::new(&format!("{:?}", ph.p_flags)), + ])); + } + + // Print the table + table.printstd(); + } +} + +fn parse_section_headers(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“ฆ Sections :".red().bold()); + + let mut table = Table::new(); + + // Add headers + table.add_row(Row::new(vec![ + Cell::new("Index"), + Cell::new("Name"), + Cell::new("Offset"), + Cell::new("Size"), + Cell::new("Flags"), + Cell::new("Entropy"), + ])); + + for (i, section) in elf.section_headers.iter().enumerate() { + if let Some(name) = elf.shdr_strtab.get_at(section.sh_name) { + let start = section.sh_offset as usize; + let end = start + section.sh_size as usize; + + // Calculate entropy if section data is in bounds + let entropy = if end <= bytes.len() { + calculate_entropy(&bytes[start..end]) + } else { + 0.0 + }; + + table.add_row(Row::new(vec![ + Cell::new(&i.to_string()), + Cell::new(name), + Cell::new(§ion.sh_offset.to_string()), + Cell::new(§ion.sh_size.to_string()), + Cell::new(&format!("{:?}", section.sh_flags)), + Cell::new(&format!("{:.2}", entropy)), + ])); + } + } + + table.printstd(); + } +} + + +fn parse_symbol_tables(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“š Symbol Tables :".red().bold()); + + + let mut table = Table::new(); + + // Add headers + table.add_row(Row::new(vec![ + Cell::new("Symbol"), + Cell::new("Address"), + Cell::new("Size"), + Cell::new("Bind"), + Cell::new("Type"), + ])); + + for sym in elf.syms.iter() { + if let Some(name) = elf.strtab.get_at(sym.st_name) { + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("0x{:x}", sym.st_value)), + Cell::new(&sym.st_size.to_string()), + Cell::new(&format!("{:?}", sym.st_bind())), + Cell::new(&format!("{:?}", sym.st_type())), + ])); + } + } + table.printstd(); + } +} + +fn parse_string_tables(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“œ String Tables :".red().bold()); + println!("{}","\n --strtab Contents :".blue().bold()); + if let Ok(strtab_vec) = elf.strtab.to_vec() { + let mut table = Table::new(); + + // Add header + table.add_row(Row::new(vec![ + Cell::new("Index"), + Cell::new("String"), + ])); + + for (i, s) in strtab_vec.iter().enumerate() { + table.add_row(Row::new(vec![ + Cell::new(&i.to_string()), + Cell::new(s), + ])); + } + table.printstd(); + } else { + println!("Failed to parse .strtab"); + } + + println!("{}","\n --.dynstr Contents :".blue().bold()); + if let Ok(dynstrtab_vec) = elf.dynstrtab.to_vec() { + + let mut table = Table::new(); + + // Add header + table.add_row(Row::new(vec![ + Cell::new("Index"), + Cell::new("String"), + ])); + + + for (i, s) in dynstrtab_vec.iter().enumerate() { + table.add_row(Row::new(vec![ + Cell::new(&i.to_string()), + Cell::new(s), + ])); + } + table.printstd(); + } else { + println!("Failed to parse .dynstrtab"); + } + } +} + + + + +fn parse_relocations(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“Œ Relocation Entries :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Offset"), + Cell::new("Type"), + Cell::new("Symbol Index"), + ])); + + for rel in elf.dynrelas.iter().chain(elf.dynrels.iter()) { + table.add_row(Row::new(vec![ + Cell::new(&format!("0x{:x}", rel.r_offset)), + Cell::new(&rel.r_type.to_string()), + Cell::new(&rel.r_sym.to_string()), + ])); + } + table.printstd(); + } +} + + + + +fn parse_dynamic_section(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“ฆ Dynamic Entries :".red().bold()); + + if let Some(dyns) = &elf.dynamic { + + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Tag"), + Cell::new("Value / Name"), + ])); + + + + for dyn_ in dyns.dyns.iter() { + let row = match dyn_.d_tag { + DT_NEEDED => { + let name = elf.dynstrtab.get_at(dyn_.d_val as usize) + .unwrap_or(""); + Row::new(vec![ + Cell::new("DT_NEEDED"), + Cell::new(name), + ]) + } + DT_INIT => Row::new(vec![ + Cell::new("DT_INIT"), + Cell::new(&format!("0x{:x}", dyn_.d_val)), + ]), + DT_FINI => Row::new(vec![ + Cell::new("DT_FINI"), + Cell::new(&format!("0x{:x}", dyn_.d_val)), + ]), + _ => continue, + }; + table.add_row(row); + } + table.printstd(); + } + } +} + + + +fn parse_hash_tables(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿงฎ Hash Sections :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Section Name"), + Cell::new("Offset"), + Cell::new("Size"), + ])); + + for section in &elf.section_headers { + if let Some(name) = elf.shdr_strtab.get_at(section.sh_name) { + if name == ".gnu.hash" || name == ".hash" { + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("{}", section.sh_offset)), + Cell::new(&format!("{}", section.sh_size)), + ])); + } + } + } + table.printstd(); + } +} + + + +fn parse_notes(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿ“ Note Sections :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Section Name"), + Cell::new("Offset"), + Cell::new("Size"), + ])); + + for section in elf.section_headers.iter() { + if let Some(name) = elf.shdr_strtab.get_at(section.sh_name) { + if name.starts_with(".note") { + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("{}", section.sh_offset)), + Cell::new(&format!("{}", section.sh_size)), + ])); + } + } + } + table.printstd(); + } +} + + +fn parse_versioning_sections(bytes: &[u8]) { + if let Ok(elf) = Elf::parse(bytes) { + println!("{}","\n\n๐Ÿงฌ Versioning Sections :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Section Name"), + Cell::new("Offset"), + Cell::new("Size"), + ])); + + for section in elf.section_headers.iter() { + if let Some(name) = elf.shdr_strtab.get_at(section.sh_name) { + if name.starts_with(".gnu.version") { + table.add_row(Row::new(vec![ + Cell::new(name), + Cell::new(&format!("{}", section.sh_offset)), + Cell::new(&format!("{}", section.sh_size)), + ])); + } + } + } + table.printstd(); + } +} + + +pub fn parse_and_print_dwarf_functions(bytes: &[u8]) -> Result<(), Box> { + let elf = Elf::parse(bytes)?; // Parse ELF file + let endian = LittleEndian; // Set little endian format + + // Helper to extract a section + let get_section = |name: &str| -> Result<&[u8], &'static str> { + for section in &elf.section_headers { + if let Some(sec_name) = elf.shdr_strtab.get_at(section.sh_name) { + if sec_name == name { + let start = section.sh_offset as usize; + let end = start + section.sh_size as usize; + return bytes.get(start..end).ok_or("Section out of bounds"); + } + } + } + Err("Section not found") + }; + + // Load DWARF sections + let debug_info_data = get_section(".debug_info")?; + let debug_abbrev_data = get_section(".debug_abbrev")?; + let debug_str_data = get_section(".debug_str")?; + + let debug_info = DebugInfo::new(debug_info_data, endian); + let debug_abbrev = DebugAbbrev::new(debug_abbrev_data, endian); + let debug_str = DebugStr::new(debug_str_data, endian); + + println!("{}","\n\n๐Ÿž Functions in DWARF Debug Info :".red().bold()); + + let mut table = Table::new(); + table.add_row(Row::new(vec![ + Cell::new("Function Name"), + Cell::new("File Index"), + Cell::new("Line Number"), + ])); + + // Iterate over Compilation Units (CUs) + let mut iter = debug_info.units(); + while let Some(header) = iter.next()? { + + + // Create a Dwarf instance + let dwarf = gimli::Dwarf { + debug_info, + debug_abbrev, + debug_str, + debug_line: DebugLine::new(get_section(".debug_line")?, endian), + ..Default::default() // Fill other fields with default values if not used + }; + + let unit = dwarf.unit(header)?; + + let mut entries = unit.entries(); // Get entries for the CU + + // Iterate over the entries in the current CU + while let Some((_, entry)) = entries.next_dfs()? { + if entry.tag() == DwTag(0x2e) /* DW_TAG_subprogram */ { + let mut name = None; + let mut file = None; + let mut line = None; + + // Iterate over the attributes of the entry + let mut attrs = entry.attrs(); // Get the iterator for the entry's attributes + while let Some(attr) = attrs.next()? { + match attr.name() { + gimli::DW_AT_name => { + if let Some(val) = attr.string_value(&debug_str) { + name = Some(val.to_string_lossy().into_owned()); + } + } + gimli::DW_AT_decl_file => { + file = attr.udata_value(); + } + gimli::DW_AT_decl_line => { + line = attr.udata_value(); + } + _ => {} + } + } + + // If the function name is found, print the result + if let Some(name) = name { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new(&format!("{}", file.unwrap_or(0))), + Cell::new(&format!("{}", line.unwrap_or(0))), + ])); + } + } + } + } + + table.printstd(); + Ok(()) +} diff --git a/src/static_analysis/mod.rs b/src/static_analysis/mod.rs index 98fb28e..e56976a 100644 --- a/src/static_analysis/mod.rs +++ b/src/static_analysis/mod.rs @@ -3,9 +3,13 @@ pub mod callgraph; pub mod decompilation; pub mod disassembly; pub mod metadata; +pub mod binary_parsing; // Re-export key functions for convenience pub use callgraph::analyze_callgraph; pub use decompilation::decompile_binary; pub use disassembly::disassemble_binary; pub use metadata::extract_metadata; +pub use binary_parsing::analyze_binary; + + From c898147827edc219124af764695c0b5621e451a6 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 09:53:59 +0530 Subject: [PATCH 21/27] chore: update release.yml --- .github/workflows/release.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c7b878..0c72b1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,9 +103,12 @@ jobs: else git log --pretty=format:"* %s (%h)" $PREVIOUS_TAG..HEAD > RELEASE_NOTES.md fi - echo "RELEASE_NOTES<> $GITHUB_ENV - cat RELEASE_NOTES.md >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV + + { + echo "RELEASE_NOTES<> $GITHUB_ENV - name: Check if development release id: check_dev @@ -118,7 +121,7 @@ jobs: - name: Create Release id: create_release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 06aac3a7c6585708dbcbaa6d43a15fe0181e5cd2 Mon Sep 17 00:00:00 2001 From: Aswinr24 Date: Sat, 10 May 2025 10:17:23 +0530 Subject: [PATCH 22/27] chore: update release.yml --- .github/workflows/release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c72b1e..788209b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,11 +104,9 @@ jobs: git log --pretty=format:"* %s (%h)" $PREVIOUS_TAG..HEAD > RELEASE_NOTES.md fi - { - echo "RELEASE_NOTES<> $GITHUB_ENV + echo "RELEASE_NOTES<> $GITHUB_ENV + cat RELEASE_NOTES.md >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: Check if development release id: check_dev @@ -121,7 +119,7 @@ jobs: - name: Create Release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 7df1d3197253431d01b28ded921227a0d63ba043 Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Sat, 10 May 2025 10:19:11 +0530 Subject: [PATCH 23/27] Update README.md --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/README.md b/README.md index e69de29..224fe11 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,91 @@ +# RustyBox: Open-Source CLI-Based Malware Analysis Sandbox + +**RustyBox** is a high-performance, Rust-powered malware analysis framework designed for secure, static binary examination. It integrates leading open-source libraries to simplify the malware analysis workflow for security researchers and professionals. + +## Features + +- **Static Analysis Focused** โ€“ Deep disassembly, callgraph generation, and unpacking using tools like Capstone, Radeco, LIEF, and Capa. +- **Modular & CLI-Based** โ€“ Flexible CLI interface with multiple analysis modes. +- **High Performance** โ€“ Built in Rust for speed, safety, and low memory overhead. +- **Pre-Configured Flow** โ€“ No manual setup needed for tools like Radare2, LIEF, or CAPA. +- **Cross-Platform** โ€“ Supports x86_64 and ARM64 binaries (Linux). + +## Commands + +### 1. Run Basic Static Analysis + +cargo run -- malware.exe + + +* Disassembles the binary and generates a callgraph in ASCII. +* Uses Radare2 to produce function flow and structure. + +### 2. Run Binary Parsing Mode + + +cargo run -- malware.exe binaryp + + +* Parses metadata, PE header info, imports/exports, and section details using LIEF. + +### 3. Enable Verbose Mode (for deeper insights) + + +cargo run -- malware.exe -v + + +* Provides verbose logging for debugging or educational output. + +## Installation + +### Requirements + +* Rust & Cargo โ€“ [Install Rust](https://rustup.rs) +* Radare2 โ€“ [GitHub: radareorg/radare2](https://github.com/radareorg/radare2) +* Graph-Easy โ€“ Install using `cpanm Graph::Easy` +* Python3 โ€“ Required for CAPA +* Docker โ€“ *(Optional)* For future dynamic analysis integration + +### Clone and Build + + +git clone https://github.com/yourusername/rustybox.git +cd rustybox +cargo build --release + + +## Architecture + +* **Disassembly** โ€“ Capstone, Radare2 +* **Decompilation** โ€“ Radeco +* **Signature Matching** โ€“ Capa +* **Binary Parsing** โ€“ LIEF +* **Visualization** โ€“ Graph-Easy, CFG rendering +* **(Upcoming)** โ€“ Firecracker/QEMU for isolated dynamic execution + +## Example Output + + +Call Graph (ASCII): +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ main โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ call sub1 โ”‚ +โ”‚ call sub2 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + +## Contributing + +We welcome pull requests and feature ideas. Please submit issues and suggestions in the [GitHub Issues](https://github.com/yourusername/rustybox/issues) section. + +## License + +Apache 2.0 โ€“ Free to use, modify, and distribute. + +--- + +**RustyBox** โ€“ Built for the community. Securing binaries, one disassembly at a time. + + +``` From d22f8535004c99facf2dccfda304ae0b05bd7533 Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Sat, 10 May 2025 11:00:58 +0530 Subject: [PATCH 24/27] Update release.yml --- .github/workflows/release.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 788209b..47d5289 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,20 +94,6 @@ jobs: with: path: artifacts - - name: Generate Release Notes - id: generate_notes - run: | - PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") - if [ -z "$PREVIOUS_TAG" ]; then - git log --pretty=format:"* %s (%h)" > RELEASE_NOTES.md - else - git log --pretty=format:"* %s (%h)" $PREVIOUS_TAG..HEAD > RELEASE_NOTES.md - fi - - echo "RELEASE_NOTES<> $GITHUB_ENV - cat RELEASE_NOTES.md >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - name: Check if development release id: check_dev run: | @@ -126,15 +112,10 @@ jobs: tag_name: ${{ github.ref_name }} name: ${{ steps.check_dev.outputs.is_dev == 'true' && format('Development Release {0}', github.ref_name) || format('Release {0}', github.ref_name) }} body: | - ## Release Notes - - ${{ env.RELEASE_NOTES }} - ## Binary Downloads The following binaries are available for this release: * Linux (amd64) - * Windows (amd64) * macOS (amd64) * macOS (arm64) From 74a1a886d3a89b984777f1a0856b58ee177e6328 Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Sat, 10 May 2025 11:32:58 +0530 Subject: [PATCH 25/27] Update README.md --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 224fe11..1d2c764 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ### 1. Run Basic Static Analysis -cargo run -- malware.exe +rustybox -- malware.exe * Disassembles the binary and generates a callgraph in ASCII. @@ -23,7 +23,7 @@ cargo run -- malware.exe ### 2. Run Binary Parsing Mode -cargo run -- malware.exe binaryp +rustybox -- malware.exe binaryp * Parses metadata, PE header info, imports/exports, and section details using LIEF. @@ -31,7 +31,7 @@ cargo run -- malware.exe binaryp ### 3. Enable Verbose Mode (for deeper insights) -cargo run -- malware.exe -v +rustybox -- malware.exe -v * Provides verbose logging for debugging or educational output. @@ -49,8 +49,10 @@ cargo run -- malware.exe -v ### Clone and Build -git clone https://github.com/yourusername/rustybox.git +git clone https://github.com/Aswinr24/rustybox.git + cd rustybox + cargo build --release @@ -63,17 +65,6 @@ cargo build --release * **Visualization** โ€“ Graph-Easy, CFG rendering * **(Upcoming)** โ€“ Firecracker/QEMU for isolated dynamic execution -## Example Output - - -Call Graph (ASCII): -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ main โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ call sub1 โ”‚ -โ”‚ call sub2 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - ## Contributing From 28d5fe511478b09acfd45bcf0a47835b3585341e Mon Sep 17 00:00:00 2001 From: Ritisha Bhattacharjee <157876598+ritishab0209@users.noreply.github.com> Date: Sat, 10 May 2025 11:33:22 +0530 Subject: [PATCH 26/27] Update README.md --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 1d2c764..448b60c 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,3 @@ We welcome pull requests and feature ideas. Please submit issues and suggestions ## License Apache 2.0 โ€“ Free to use, modify, and distribute. - ---- - -**RustyBox** โ€“ Built for the community. Securing binaries, one disassembly at a time. - - -``` From b1caa4874f7c0c5d4d7d195984d5e0114d7c32e3 Mon Sep 17 00:00:00 2001 From: R Aswin <135364633+Aswinr24@users.noreply.github.com> Date: Wed, 22 Oct 2025 23:16:21 +0530 Subject: [PATCH 27/27] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 448b60c..9f19747 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RustyBox: Open-Source CLI-Based Malware Analysis Sandbox +# RustyBox โ€“ Fast, Modular CLI Malware Analysis **RustyBox** is a high-performance, Rust-powered malware analysis framework designed for secure, static binary examination. It integrates leading open-source libraries to simplify the malware analysis workflow for security researchers and professionals. @@ -73,3 +73,4 @@ We welcome pull requests and feature ideas. Please submit issues and suggestions ## License Apache 2.0 โ€“ Free to use, modify, and distribute. +