diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..4fb22c7 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,24 @@ +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: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + - 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..9ba6043 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,19 @@ +name: Lint + +on: + pull_request: + branches: [main] + +jobs: + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + 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/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..47d5289 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,128 @@ +name: Release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+*" + - "dev-v[0-9]+.[0-9]+.[0-9]+*" + +jobs: + 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_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: 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 }} + + - 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 + + - name: Build binary + run: cargo build --release --target ${{ matrix.target }} + + - 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 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset_name }} + path: release/${{ matrix.asset_name }} + if-no-files-found: error + + create_release: + name: Create GitHub Release + needs: build_release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - 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 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + 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: | + ## Binary Downloads + + The following binaries are available for this release: + * Linux (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: ${{ steps.check_dev.outputs.is_dev }} + files: | + artifacts/rustybox-linux-amd64/rustybox-linux-amd64 + artifacts/rustybox-macos-amd64/rustybox-macos-amd64 + artifacts/rustybox-macos-arm64/rustybox-macos-arm64 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 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 new file mode 100644 index 0000000..803051f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rustybox" +version = "0.1.0" +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" +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" +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/README.md b/README.md new file mode 100644 index 0000000..9f19747 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# 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. + +## 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 + +rustybox -- malware.exe + + +* Disassembles the binary and generates a callgraph in ASCII. +* Uses Radare2 to produce function flow and structure. + +### 2. Run Binary Parsing Mode + + +rustybox -- malware.exe binaryp + + +* Parses metadata, PE header info, imports/exports, and section details using LIEF. + +### 3. Enable Verbose Mode (for deeper insights) + + +rustybox -- 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/Aswinr24/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 + + +## 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. + 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 new file mode 100644 index 0000000..9ed748a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,648 @@ +use clap::{Arg, Command}; +use colored::Colorize; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + 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::BorderType, + widgets::{Block, Borders, List, ListItem, Paragraph, Tabs}, +}; +use std::{ + io, + path::Path, + sync::mpsc, + thread, + time::{Duration, Instant}, +}; + +use crate::static_analysis::{ + analyze_callgraph, decompile_binary, disassemble_binary, extract_metadata, analyze_binary, +}; + +const RUSTYBOX_ASCII: &str = r#" + ____ _ ____ +| _ \ _ _ ___| |_ _ _| __ ) _____ __ +| |_) | | | / __| __| | | | _ \ / _ \ \/ / +| _ <| |_| \__ \ |_| |_| | |_) | (_) > < +|_| \_\\__,_|___/\__|\__, |____/ \___/_/\_\ + |___/ + Express Binary Analysis Tool +"#; + +enum AnalysisMode { + Metadata, + Disassembly, + Decompile, + Callgraph, +} + +struct AnalysisResult { + metadata: Option>, + disassembly: Option>, + decompile: Option>, + callgraph: Option>, +} + +#[allow(dead_code)] +struct App { + selected_tab: usize, + analysis_results: AnalysisResult, + file_path: String, + instr_count: u32, + verbose: bool, + scroll_position: u16, +} + +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, + scroll_position: 0, + } + } + + 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(), + } + } + 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> { + let matches = create_cli().get_matches(); + + let file_path = matches.get_one::("FILE").unwrap(); + + if !Path::new(file_path).exists() { + return Err(format!("File does not exist: {}", file_path).into()); + } + 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); + } + + // 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.truecolor(225, 95, 80).bold()); + + + if matches.get_flag("binaryp") { + if let Err(e) = crate::static_analysis::analyze_binary(file_path) { + eprintln!("Error analyzing binary: {}", e); + } + return Ok(()); // Exit after running the binaryp command + } + + + 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> { + 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)?; + + let mut app = App::new(file_path.clone(), instr_count, verbose); + + let (tx, rx) = mpsc::channel(); + let file_path_clone = file_path.clone(); + let instr_count_clone = instr_count; + let verbose_clone = verbose; + + 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 | 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(), + _ => {} + } + } + } + + if last_tick.elapsed() >= tick_rate { + last_tick = Instant::now(); + } + } + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) +} + +fn draw_ui(f: &mut ratatui::Frame, app: &App) { + let terminal_size = f.area(); + + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(1), + ]) + .split(terminal_size); + + let top_bar = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) + .split(main_layout[0]); + + let file_info = Paragraph::new(format!(" {}", app.file_path)) + .style(Style::default().fg(Color::Cyan)) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Target"), + ); + + f.render_widget(file_info, top_bar[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) + .border_type(BorderType::Rounded), + ) + .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, top_bar[1]); + + // 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) + .border_type(BorderType::Rounded) + .title(format!("{}{}", content_title, scroll_info)), + ) + .style(Style::default().fg(Color::White)) + .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), + ), + ]); + + let help_bar = Paragraph::new(help_text).style(Style::default().bg(Color::DarkGray)); + + f.render_widget(help_bar, main_layout[2]); +} + +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), + ) + + .arg( + Arg::new("binaryp") + .long("binaryp") + .help("Analyze the binary using the binaryp command") + .action(clap::ArgAction::SetTrue), + ) +} 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/main.rs b/src/main.rs new file mode 100644 index 0000000..b8f9073 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +mod cli; +mod static_analysis; +mod utils; + +fn main() { + if let Err(e) = cli::run() { + eprintln!("Error: {e}"); + std::process::exit(1); + } +} diff --git a/src/reporting/json.rs b/src/reporting/json.rs new file mode 100644 index 0000000..e69de29 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/callgraph.rs b/src/static_analysis/callgraph.rs new file mode 100644 index 0000000..5defd20 --- /dev/null +++ b/src/static_analysis/callgraph.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::process::{Command, Stdio}; + +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, + ]) + .stdout(Stdio::piped()) + .output()?; + + if !output.status.success() { + return Err("Failed to generate callgraph using radare2".into()); + } + + // Write output to DOT file + fs::write(dot_path, &output.stdout)?; + + // Step 2: Convert DOT to ASCII using graph-easy + let graph_output = Command::new("graph-easy") + .arg(dot_path) + .stdout(Stdio::piped()) + .output()?; + + // Cleanup DOT file + fs::remove_file(dot_path).ok(); + + 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(&graph_output.stderr).to_string(); + Err( + format!("graph-easy error:\n{err_msg}\nHint: Install it using `cpanm Graph::Easy`") + .into(), + ) + } +} diff --git a/src/static_analysis/decompilation.rs b/src/static_analysis/decompilation.rs new file mode 100644 index 0000000..8521b03 --- /dev/null +++ b/src/static_analysis/decompilation.rs @@ -0,0 +1,69 @@ +use log::{debug, error, info}; +use r2pipe::R2Pipe; + +/// Use Radare2's native decompiler (pdd) and return the decompiled output as a String +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) +} diff --git a/src/static_analysis/disassembly.rs b/src/static_analysis/disassembly.rs new file mode 100644 index 0000000..d836f61 --- /dev/null +++ b/src/static_analysis/disassembly.rs @@ -0,0 +1,54 @@ +use log::{debug, info}; +use r2pipe::R2Pipe; +use serde_json::Value; + +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"); + 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 = { "aaa" }; + 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 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(""); + output.push_str(&format!("{offset:#x}:\t{mnemonic}\t{opcode}\n")); + } + + r2.close(); + Ok(output) +} diff --git a/src/static_analysis/metadata.rs b/src/static_analysis/metadata.rs new file mode 100644 index 0000000..a96195f --- /dev/null +++ b/src/static_analysis/metadata.rs @@ -0,0 +1,68 @@ +use goblin::Object; +use std::convert::TryInto; +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.try_into().unwrap_or(0)); + metadata.sections = Some(pe.sections.len()); + 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) => { + 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.try_into().unwrap_or(0)); + 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) +} diff --git a/src/static_analysis/mod.rs b/src/static_analysis/mod.rs new file mode 100644 index 0000000..e56976a --- /dev/null +++ b/src/static_analysis/mod.rs @@ -0,0 +1,15 @@ +// Export all modules +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; + + 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) +} diff --git a/src/utils/logging.rs b/src/utils/logging.rs new file mode 100644 index 0000000..a98cef1 --- /dev/null +++ b/src/utils/logging.rs @@ -0,0 +1,43 @@ +use log::{LevelFilter, info}; +use simplelog::{CombinedLogger, TermLogger, WriteLogger, TerminalMode, Config, ColorChoice}; +use std::fs::File; +use std::path::Path; + +pub fn init_logging(log_file: Option<&Path>, 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 diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +