diff --git a/.gitignore b/.gitignore index d1f6ee3..942c01a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ *.coverage .env .angelusvigil/ +.worktrees/ PROJECTS/beginner/hash-cracker/docs/ diff --git a/PROJECTS/intermediate/binary-analysis-tool/.env.example b/PROJECTS/intermediate/binary-analysis-tool/.env.example new file mode 100644 index 0000000..6c3bc7f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/.env.example @@ -0,0 +1,19 @@ +# ©AngelaMos | 2026 +# .env.example +# Copy to .env (production) or .env.development (dev) and adjust values + +APP_NAME=axumortem +VITE_API_URL=/api +VITE_APP_TITLE=axumortem + +NGINX_HOST_PORT=22784 +FRONTEND_HOST_PORT=15723 +BACKEND_HOST_PORT=3000 +POSTGRES_HOST_PORT=5432 + +POSTGRES_PASSWORD=changeme +RUST_LOG=info +MAX_UPLOAD_SIZE=52428800 +CORS_ORIGIN=* + +# CLOUDFLARE_TUNNEL_TOKEN=your-token-here diff --git a/PROJECTS/intermediate/binary-analysis-tool/.gitignore b/PROJECTS/intermediate/binary-analysis-tool/.gitignore new file mode 100644 index 0000000..97c020f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/.gitignore @@ -0,0 +1,24 @@ +# ©AngelaMos | 2026 +# .gitignore + +# Rust +backend/target/ + +# Environment +.env +.env.development +.env.production +.env.local +!.env.example + +# Development docs +docs/ + +# OS +.DS_Store +Thumbs.db + +# Editors +.idea/ +.vscode/ +*.sw? diff --git a/PROJECTS/intermediate/binary-analysis-tool/README.md b/PROJECTS/intermediate/binary-analysis-tool/README.md new file mode 100644 index 0000000..23d0bf0 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/README.md @@ -0,0 +1,65 @@ +```rust + █████╗ ██╗ ██╗██╗ ██╗███╗ ███╗ ██████╗ ██████╗ ████████╗███████╗███╗ ███╗ +██╔══██╗╚██╗██╔╝██║ ██║████╗ ████║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝████╗ ████║ +███████║ ╚███╔╝ ██║ ██║██╔████╔██║██║ ██║██████╔╝ ██║ █████╗ ██╔████╔██║ +██╔══██║ ██╔██╗ ██║ ██║██║╚██╔╝██║██║ ██║██╔══██╗ ██║ ██╔══╝ ██║╚██╔╝██║ +██║ ██║██╔╝ ██╗╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║ ██║ ██║ ███████╗██║ ╚═╝ ██║ +╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +``` + +[![Cybersecurity Projects](https://img.shields.io/badge/Cybersecurity--Projects-Project%20%2320-red?style=flat&logo=github)](https://github.com/CarterPerez-dev/Cybersecurity-Projects/tree/main/PROJECTS/intermediate/binary-analysis-tool) +[![Rust](https://img.shields.io/badge/Rust-stable-000000?style=flat&logo=rust&logoColor=white)](https://www.rust-lang.org) +[![React](https://img.shields.io/badge/React-19-61DAFB?style=flat&logo=react&logoColor=black)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![License: AGPLv3](https://img.shields.io/badge/License-AGPL_v3-purple.svg)](https://www.gnu.org/licenses/agpl-3.0) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?style=flat&logo=docker)](https://www.docker.com) + +> Static binary analysis engine with multi-format parsing, YARA scanning, x86 disassembly, and MITRE ATT&CK threat scoring. + +*This is a quick overview — security theory, architecture, and full walkthroughs are in the [learn modules](#learn).* + +## What It Does + +- Multi-format binary parsing (ELF, PE, Mach-O) with section analysis and import table extraction +- YARA rule scanning with 14 built-in detection rules for malware, packers, and crypto patterns +- x86/x86_64 disassembly with control flow graph generation from entry points and symbol tables +- Shannon entropy analysis for detecting packed or encrypted sections +- 8-category threat scoring system (max 100 points) with MITRE ATT&CK technique mapping +- Pass-based analysis pipeline with topological ordering and dependency resolution + +## Quick Start + +```bash +docker compose up -d +``` + +Visit `http://localhost:22784` + +> [!TIP] +> This project uses [`just`](https://github.com/casey/just) as a command runner. Type `just` to see all available commands. +> +> Install: `curl -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin` + +## Stack + +**Backend:** Rust, Axum, goblin, iced-x86, yara-x, SQLx, PostgreSQL + +**Frontend:** React 19, TypeScript, Vite, TanStack Query, Zustand, Zod, SCSS Modules + +**Infra:** Docker Compose, Nginx, PostgreSQL 18 + +## Learn + +This project includes step-by-step learning materials covering security theory, architecture, and implementation. + +| Module | Topic | +|--------|-------| +| [00 - Overview](learn/00-OVERVIEW.md) | Prerequisites and quick start | +| [01 - Concepts](learn/01-CONCEPTS.md) | Security theory and real-world breaches | +| [02 - Architecture](learn/02-ARCHITECTURE.md) | System design and data flow | +| [03 - Implementation](learn/03-IMPLEMENTATION.md) | Code walkthrough | +| [04 - Challenges](learn/04-CHALLENGES.md) | Extension ideas and exercises | + +## License + +AGPL 3.0 diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.lock b/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.lock new file mode 100644 index 0000000..1419ef2 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.lock @@ -0,0 +1,4442 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "log", + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "array-bytes" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d55334c98d756b32dcceb60248647ab34f027690f87f9a362fd292676ee927" +dependencies = [ + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "ascii_tree" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axumortem" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "axumortem-engine", + "chrono", + "clap", + "serde", + "serde_json", + "sqlx", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "axumortem-engine" +version = "0.1.0" +dependencies = [ + "goblin", + "iced-x86", + "memmap2", + "petgraph", + "rangemap", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tracing", + "yara-x", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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 = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.31.1", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash 2.1.2", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" + +[[package]] +name = "cranelift-control" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" + +[[package]] +name = "cranelift-native" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[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 = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fmmap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099ab52d5329340a3014f60ca91bc892181ae32e752360d07be9295924dcb0b" +dependencies = [ + "byteorder", + "bytes", + "enum_dispatch", + "fs4", + "memmapix", + "parse-display", +] + +[[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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +dependencies = [ + "fallible-iterator 0.2.0", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator 0.3.0", + "indexmap 2.13.0", + "stable_deref_trait", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax 0.8.10", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[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 = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "iced-x86" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "intaglio" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4271d1532513fd3671b4a768fc5f189d5365621d2fb844cfb341ef4b67afaff" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linkme" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "logos" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax 0.8.10", + "rustc_version", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "md2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4f0f3ed25ff4f8d8d102288d92f900efc202661c884cf67dfe4f0d07c43d1f" +dependencies = [ + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memmapix" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f517ab414225d5f1755bd284d9545bd08a72a3958b3c6384d72e95de9cc1a1d3" +dependencies = [ + "rustix 0.38.44", +] + +[[package]] +name = "memx" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93022fcc0eab2dc9dd134e362592e06f0b3c0aa549ae91d8b3e539e56c2a0687" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-display" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c" +dependencies = [ + "once_cell", + "parse-display-derive", + "regex", +] + +[[package]] +name = "parse-display-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68517892c8daf78da08c0db777fcc17e07f2f63ef70041718f8a7630ad84f341" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "regex", + "regex-syntax 0.7.5", + "structmeta", + "syn", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d" +dependencies = [ + "cranelift-bitset", + "log", + "sptr", + "wasmtime-math", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regalloc2" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash 2.1.2", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[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.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[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 = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn", +] + +[[package]] +name = "structmeta-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tlsh-fixed" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f762ca8308eda1e38512dc88a99f021e5214699ba133de157f588c8bfd0745c7" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "walrus" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6481311b98508f4bc2d0abbfa5d42172e7a54b4b24d8f15e28b0dc650be0c59f" +dependencies = [ + "anyhow", + "gimli 0.26.2", + "id-arena", + "leb128", + "log", + "walrus-macro", + "wasm-encoder 0.214.0", + "wasmparser 0.214.0", +] + +[[package]] +name = "walrus-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ad39ff894c43c9649fa724cdde9a6fc50b855d517ef071a93e5df82fe51d3" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.214.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff694f02a8d7a50b6922b197ae03883fbf18cdb2ae9fbee7b6148456f5f44041" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8444fe4920de80a4fe5ab564fff2ae58b6b73166b89751f8c6c93509da32e5" +dependencies = [ + "leb128", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmparser" +version = "0.214.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5309c1090e3e84dad0d382f42064e9933fdaedb87e468cc239f0eabea73ddcb6" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap 2.13.0", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7343c42a97f2926c7819ff81b64012092ae954c5d83ddd30c9fcdefd97d0b283" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmtime" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69" +dependencies = [ + "anyhow", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "hashbrown 0.14.5", + "indexmap 2.13.0", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter", + "rustix 0.38.44", + "serde", + "serde_derive", + "smallvec", + "sptr", + "target-lexicon", + "wasmparser 0.221.3", + "wasmtime-asm-macros", + "wasmtime-component-macro", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-component-macro" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.221.3", +] + +[[package]] +name = "wasmtime-component-util" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e" + +[[package]] +name = "wasmtime-cranelift" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.31.1", + "itertools 0.12.1", + "log", + "object 0.36.7", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.221.3", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.31.1", + "indexmap 2.13.0", + "log", + "object 0.36.7", + "postcard", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.221.3", + "wasmparser 0.221.3", + "wasmprinter", +] + +[[package]] +name = "wasmtime-fiber" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 0.38.44", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "wit-parser 0.221.3", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.221.3", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yara-x" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9d8d9eb43b0428341ab312b682bf3d740f49bb04706904f8fe0bd1e8fcedbf" +dependencies = [ + "aho-corasick", + "annotate-snippets", + "anyhow", + "array-bytes", + "ascii_tree", + "base64", + "bincode", + "bitflags", + "bitvec", + "bstr", + "const-oid", + "crc32fast", + "der-parser", + "digest", + "dsa", + "ecdsa", + "fmmap", + "globwalk", + "indexmap 2.13.0", + "intaglio", + "itertools 0.14.0", + "lazy_static", + "linkme", + "md-5", + "md2", + "memchr", + "memx", + "nom", + "num-derive", + "num-traits", + "p256", + "p384", + "protobuf", + "protobuf-codegen", + "protobuf-parse", + "regex", + "regex-automata", + "regex-syntax 0.8.10", + "roxmltree", + "rsa", + "rustc-hash 2.1.2", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "strum_macros", + "thiserror 2.0.18", + "tlsh-fixed", + "uuid", + "walrus", + "wasmtime", + "x509-parser", + "yansi", + "yara-x-macros", + "yara-x-parser", + "yara-x-proto", +] + +[[package]] +name = "yara-x-macros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0ab7818482696946888a759ce6e7751bfb172b624a6954577406fbb8afe7a8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "yara-x-parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e707749cd396e129c92743655c84f793410d64e32628f4a4fc2369cbb70703" +dependencies = [ + "ascii_tree", + "bitflags", + "bstr", + "indexmap 2.13.0", + "itertools 0.14.0", + "logos", + "num-traits", + "rowan", + "rustc-hash 2.1.2", + "serde", + "yansi", +] + +[[package]] +name = "yara-x-proto" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4562fd8511916a13269d453f45d5df8e6dd8bedf3d569f76e6e51d22a065b956" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protobuf-parse", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.toml b/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.toml new file mode 100644 index 0000000..115c6b7 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/Cargo.toml @@ -0,0 +1,20 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[workspace] +members = ["crates/axumortem", "crates/axumortem-engine"] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +anyhow = "1" +tracing = "0.1" +tokio = { version = "1", features = ["full"] } +sha2 = "0.10" diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/Cargo.toml b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/Cargo.toml new file mode 100644 index 0000000..3e0df11 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/Cargo.toml @@ -0,0 +1,26 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[package] +name = "axumortem-engine" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +goblin = "0.9" +iced-x86 = { version = "1.21", features = [ + "decoder", + "intel", + "nasm", + "instr_info", +] } +yara-x = "0.13" +petgraph = "0.7" +memmap2 = "0.9" +rangemap = "1.5" +sha2 = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs new file mode 100644 index 0000000..23008dd --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs @@ -0,0 +1,66 @@ +// ©AngelaMos | 2026 +// context.rs + +use std::sync::Arc; + +use memmap2::Mmap; + +use crate::formats::FormatResult; +use crate::passes::disasm::DisassemblyResult; +use crate::passes::entropy::EntropyResult; +use crate::passes::imports::ImportResult; +use crate::passes::strings::StringResult; +use crate::passes::threat::ThreatResult; + +pub enum BinarySource { + Mapped(Mmap), + Buffered(Arc<[u8]>), +} + +impl AsRef<[u8]> for BinarySource { + fn as_ref(&self) -> &[u8] { + match self { + Self::Mapped(mmap) => mmap, + Self::Buffered(buf) => buf, + } + } +} + +pub struct AnalysisContext { + source: BinarySource, + pub sha256: String, + pub file_name: String, + pub file_size: u64, + pub format_result: Option, + pub import_result: Option, + pub string_result: Option, + pub entropy_result: Option, + pub disassembly_result: Option, + pub threat_result: Option, +} + +impl AnalysisContext { + pub fn new( + source: BinarySource, + sha256: String, + file_name: String, + file_size: u64, + ) -> Self { + Self { + source, + sha256, + file_name, + file_size, + format_result: None, + import_result: None, + string_result: None, + entropy_result: None, + disassembly_result: None, + threat_result: None, + } + } + + pub fn data(&self) -> &[u8] { + self.source.as_ref() + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs new file mode 100644 index 0000000..4c0e368 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs @@ -0,0 +1,33 @@ +// ©AngelaMos | 2026 +// error.rs + +#[derive(thiserror::Error, Debug)] +pub enum EngineError { + #[error("invalid binary: {reason}")] + InvalidBinary { reason: String }, + + #[error("unsupported format: {format}")] + UnsupportedFormat { format: String }, + + #[error("unsupported architecture: {arch}")] + UnsupportedArchitecture { arch: String }, + + #[error("pass '{pass}' missing dependency: {dependency}")] + MissingDependency { + pass: String, + dependency: String, + }, + + #[error("pass '{pass}' failed")] + PassFailed { + pass: &'static str, + #[source] + source: Box, + }, + + #[error("yara error: {0}")] + Yara(String), + + #[error(transparent)] + Io(#[from] std::io::Error), +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs new file mode 100644 index 0000000..7a2fe6c --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs @@ -0,0 +1,313 @@ +// ©AngelaMos | 2026 +// elf.rs + +use goblin::elf::dynamic::DT_BIND_NOW; +use goblin::elf::header::{ + EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN, + ET_EXEC, ET_REL, +}; +use goblin::elf::program_header::{ + PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME, + PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD, PT_NOTE, + PT_NULL, PT_PHDR, +}; +use goblin::elf::section_header::{ + SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS, + SHT_SYMTAB, +}; +use goblin::elf::sym::STT_FUNC; +use goblin::elf::Elf; + +use super::{ + detect_common_anomalies, compute_section_hash, ElfInfo, + FormatResult, SectionInfo, SegmentInfo, +}; +use crate::error::EngineError; +use crate::types::{ + Architecture, BinaryFormat, Endianness, SectionPermissions, +}; + +const EI_OSABI: usize = 7; +const ELFOSABI_NONE: u8 = 0; +const ELFOSABI_HPUX: u8 = 1; +const ELFOSABI_NETBSD: u8 = 2; +const ELFOSABI_GNU: u8 = 3; +const ELFOSABI_SOLARIS: u8 = 6; +const ELFOSABI_FREEBSD: u8 = 9; +const ELFOSABI_OPENBSD: u8 = 12; +const ELFOSABI_ARM: u8 = 97; +const ELFOSABI_STANDALONE: u8 = 255; + +const DT_FLAGS: u64 = 30; +const DF_BIND_NOW: u64 = 0x8; + +pub fn parse_elf( + elf: &Elf, + data: &[u8], +) -> Result { + let architecture = + map_architecture(elf.header.e_machine); + let bits = if elf.is_64 { 64 } else { 32 }; + let endianness = if elf.little_endian { + Endianness::Little + } else { + Endianness::Big + }; + let entry_point = elf.header.e_entry; + + let has_symtab = elf + .section_headers + .iter() + .any(|sh| sh.sh_type == SHT_SYMTAB); + let is_stripped = !has_symtab; + + let has_interp = elf + .program_headers + .iter() + .any(|ph| ph.p_type == PT_INTERP); + let is_pie = + elf.header.e_type == ET_DYN && has_interp; + + let has_debug_info = + elf.section_headers.iter().any(|sh| { + elf.shdr_strtab + .get_at(sh.sh_name) + .is_some_and(|name| { + name.starts_with(".debug_") + }) + }); + + let sections = build_sections(elf, data); + let segments = build_segments(elf); + let anomalies = detect_common_anomalies( + §ions, + entry_point, + is_stripped, + ); + let elf_info = build_elf_info(elf); + let function_hints = + collect_function_hints(elf, entry_point); + + Ok(FormatResult { + format: BinaryFormat::Elf, + architecture, + bits, + endianness, + entry_point, + is_stripped, + is_pie, + has_debug_info, + sections, + segments, + anomalies, + pe_info: None, + elf_info: Some(elf_info), + macho_info: None, + function_hints, + }) +} + +fn map_architecture(machine: u16) -> Architecture { + match machine { + EM_386 => Architecture::X86, + EM_X86_64 => Architecture::X86_64, + EM_ARM => Architecture::Arm, + EM_AARCH64 => Architecture::Aarch64, + other => { + Architecture::Other(format!( + "elf-machine-{other}" + )) + } + } +} + +fn os_abi_name(abi: u8) -> String { + match abi { + ELFOSABI_NONE => "SysV".into(), + ELFOSABI_HPUX => "HP-UX".into(), + ELFOSABI_NETBSD => "NetBSD".into(), + ELFOSABI_GNU => "GNU/Linux".into(), + ELFOSABI_SOLARIS => "Solaris".into(), + ELFOSABI_FREEBSD => "FreeBSD".into(), + ELFOSABI_OPENBSD => "OpenBSD".into(), + ELFOSABI_ARM => "ARM".into(), + ELFOSABI_STANDALONE => "Standalone".into(), + other => format!("Unknown({other})"), + } +} + +fn elf_type_name(e_type: u16) -> String { + match e_type { + ET_REL => "REL".into(), + ET_EXEC => "EXEC".into(), + ET_DYN => "DYN".into(), + ET_CORE => "CORE".into(), + other => format!("Unknown({other})"), + } +} + +fn segment_type_name(p_type: u32) -> Option { + Some( + match p_type { + PT_NULL => "NULL", + PT_LOAD => "LOAD", + PT_DYNAMIC => "DYNAMIC", + PT_INTERP => "INTERP", + PT_NOTE => "NOTE", + PT_PHDR => "PHDR", + PT_GNU_EH_FRAME => "GNU_EH_FRAME", + PT_GNU_STACK => "GNU_STACK", + PT_GNU_RELRO => "GNU_RELRO", + _ => return Some(format!("0x{p_type:x}")), + } + .into(), + ) +} + +fn build_sections( + elf: &Elf, + data: &[u8], +) -> Vec { + elf.section_headers + .iter() + .skip(1) + .map(|shdr| { + let name = elf + .shdr_strtab + .get_at(shdr.sh_name) + .unwrap_or("") + .to_string(); + + let is_nobits = shdr.sh_type == SHT_NOBITS; + let raw_offset = if is_nobits { + 0 + } else { + shdr.sh_offset + }; + let raw_size = if is_nobits { + 0 + } else { + shdr.sh_size + }; + + let permissions = SectionPermissions { + read: (shdr.sh_flags + & u64::from(SHF_ALLOC)) + != 0, + write: (shdr.sh_flags + & u64::from(SHF_WRITE)) + != 0, + execute: (shdr.sh_flags + & u64::from(SHF_EXECINSTR)) + != 0, + }; + + let sha256 = compute_section_hash( + data, raw_offset, raw_size, + ); + + SectionInfo { + name, + virtual_address: shdr.sh_addr, + virtual_size: shdr.sh_size, + raw_offset, + raw_size, + permissions, + sha256, + } + }) + .collect() +} + +fn build_segments(elf: &Elf) -> Vec { + elf.program_headers + .iter() + .map(|phdr| { + let name = segment_type_name(phdr.p_type); + let permissions = SectionPermissions { + read: (phdr.p_flags & PF_R) != 0, + write: (phdr.p_flags & PF_W) != 0, + execute: (phdr.p_flags & PF_X) != 0, + }; + + SegmentInfo { + name, + virtual_address: phdr.p_vaddr, + virtual_size: phdr.p_memsz, + file_offset: phdr.p_offset, + file_size: phdr.p_filesz, + permissions, + } + }) + .collect() +} + +fn build_elf_info(elf: &Elf) -> ElfInfo { + let os_abi = + os_abi_name(elf.header.e_ident[EI_OSABI]); + let elf_type = elf_type_name(elf.header.e_type); + let interpreter = + elf.interpreter.map(|s| s.to_string()); + + let gnu_relro = elf + .program_headers + .iter() + .any(|ph| ph.p_type == PT_GNU_RELRO); + + let stack_executable = elf + .program_headers + .iter() + .find(|ph| ph.p_type == PT_GNU_STACK) + .is_some_and(|ph| (ph.p_flags & PF_X) != 0); + + let mut bind_now = false; + if let Some(dynamic) = &elf.dynamic { + for dyn_entry in &dynamic.dyns { + let tag = dyn_entry.d_tag as u64; + if tag == DT_BIND_NOW { + bind_now = true; + } + if tag == DT_FLAGS + && (dyn_entry.d_val & DF_BIND_NOW) != 0 + { + bind_now = true; + } + } + } + + let needed_libraries = elf + .libraries + .iter() + .map(|s| s.to_string()) + .collect(); + + ElfInfo { + os_abi, + elf_type, + interpreter, + gnu_relro, + bind_now, + stack_executable, + needed_libraries, + } +} + +fn collect_function_hints( + elf: &Elf, + entry_point: u64, +) -> Vec { + let mut hints: Vec = elf + .syms + .iter() + .chain(elf.dynsyms.iter()) + .filter(|sym| { + sym.st_type() == STT_FUNC + && sym.st_value != 0 + && sym.st_value != entry_point + }) + .map(|sym| sym.st_value) + .collect(); + hints.sort_unstable(); + hints.dedup(); + hints +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs new file mode 100644 index 0000000..59a33e1 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs @@ -0,0 +1,361 @@ +// ©AngelaMos | 2026 +// macho.rs + +use goblin::mach::cputype::{ + CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86, + CPU_TYPE_X86_64, +}; +use goblin::mach::load_command::CommandVariant; +use goblin::mach::{Mach, MachO}; + +use super::{ + compute_section_hash, detect_common_anomalies, + FormatResult, MachOInfo, SectionInfo, SegmentInfo, +}; +use crate::error::EngineError; +use crate::types::{ + Architecture, BinaryFormat, Endianness, SectionPermissions, +}; + +const MH_OBJECT: u32 = 1; +const MH_EXECUTE: u32 = 2; +const MH_DYLIB: u32 = 6; +const MH_BUNDLE: u32 = 8; +const MH_DSYM: u32 = 10; +const MH_KEXT_BUNDLE: u32 = 11; + +const VM_PROT_READ: u32 = 0x01; +const VM_PROT_WRITE: u32 = 0x02; +const VM_PROT_EXECUTE: u32 = 0x04; + +pub fn parse_macho( + mach: &Mach, + data: &[u8], +) -> Result { + match mach { + Mach::Binary(macho) => { + parse_single_macho(macho, data, false) + } + Mach::Fat(fat) => { + for arch in fat.iter_arches() { + let arch = arch.map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + let offset = arch.offset as usize; + let size = arch.size as usize; + let end = offset.saturating_add(size); + if end <= data.len() { + let macho = MachO::parse(data, offset) + .map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + return parse_single_macho( + &macho, data, true, + ); + } + } + Err(EngineError::InvalidBinary { + reason: "no valid architecture in \ + universal binary" + .into(), + }) + } + } +} + +fn parse_single_macho( + macho: &MachO, + data: &[u8], + is_universal: bool, +) -> Result { + let architecture = + map_architecture(macho.header.cputype); + let bits = if macho.is_64 { 64 } else { 32 }; + let endianness = if macho.little_endian { + Endianness::Little + } else { + Endianness::Big + }; + let entry_point = macho.entry; + + let symbols: Vec<_> = + macho.symbols().flatten().collect(); + let is_stripped = symbols.is_empty(); + + let has_debug_info = macho.segments.iter().any(|seg| { + seg.name().is_ok_and(|n| n == "__DWARF") + }); + + let is_pie = macho.header.flags & 0x0020_0000 != 0; + + let sections = build_sections(macho, data); + let segments = build_segments(macho); + let anomalies = detect_common_anomalies( + §ions, + entry_point, + is_stripped, + ); + let macho_info = + build_macho_info(macho, is_universal); + + let function_hints: Vec = macho + .symbols() + .flatten() + .filter(|(_, nlist)| { + !nlist.is_stab() + && nlist.n_type & 0x0e == 0x0e + && nlist.n_value != 0 + && nlist.n_value != entry_point + }) + .map(|(_, nlist)| nlist.n_value) + .collect(); + + Ok(FormatResult { + format: BinaryFormat::MachO, + architecture, + bits, + endianness, + entry_point, + is_stripped, + is_pie, + has_debug_info, + sections, + segments, + anomalies, + pe_info: None, + elf_info: None, + macho_info: Some(macho_info), + function_hints, + }) +} + +fn map_architecture(cputype: u32) -> Architecture { + match cputype { + CPU_TYPE_X86 => Architecture::X86, + CPU_TYPE_X86_64 => Architecture::X86_64, + CPU_TYPE_ARM => Architecture::Arm, + CPU_TYPE_ARM64 => Architecture::Aarch64, + other => { + Architecture::Other(format!( + "mach-cpu-{other:#x}" + )) + } + } +} + +fn file_type_name(filetype: u32) -> String { + match filetype { + MH_OBJECT => "Object".into(), + MH_EXECUTE => "Execute".into(), + MH_DYLIB => "Dylib".into(), + MH_BUNDLE => "Bundle".into(), + MH_DSYM => "Dsym".into(), + MH_KEXT_BUNDLE => "Kext".into(), + other => format!("Unknown({other})"), + } +} + +fn cpu_subtype_name( + cputype: u32, + cpusubtype: u32, +) -> String { + let subtype = cpusubtype & 0x00FF_FFFF; + match cputype { + CPU_TYPE_X86 | CPU_TYPE_X86_64 => { + match subtype { + 3 => "ALL".into(), + 4 => "486".into(), + 8 => "PENTIUM_3".into(), + 9 => "PENTIUM_M".into(), + 10 => "PENTIUM_4".into(), + 11 => "ITANIUM".into(), + 12 => "XEON".into(), + _ => format!("{subtype}"), + } + } + CPU_TYPE_ARM => match subtype { + 6 => "v6".into(), + 9 => "v7".into(), + 11 => "v7f".into(), + 12 => "v7s".into(), + 13 => "v7k".into(), + _ => format!("{subtype}"), + }, + CPU_TYPE_ARM64 => match subtype { + 0 => "ALL".into(), + 1 => "v8".into(), + 2 => "E".into(), + _ => format!("{subtype}"), + }, + _ => format!("{subtype}"), + } +} + +fn build_sections( + macho: &MachO, + data: &[u8], +) -> Vec { + let mut sections = Vec::new(); + for segment in macho.segments.iter() { + let initprot = segment.initprot; + let seg_permissions = SectionPermissions { + read: (initprot & VM_PROT_READ) != 0, + write: (initprot & VM_PROT_WRITE) != 0, + execute: (initprot & VM_PROT_EXECUTE) + != 0, + }; + for section_result in segment.into_iter() { + let Ok((section, _section_data)) = + section_result + else { + continue; + }; + let name = section + .name() + .unwrap_or("???") + .to_string(); + let raw_offset = section.offset as u64; + let raw_size = section.size; + let sha256 = compute_section_hash( + data, raw_offset, raw_size, + ); + + sections.push(SectionInfo { + name, + virtual_address: section.addr, + virtual_size: section.size, + raw_offset, + raw_size, + permissions: seg_permissions.clone(), + sha256, + }); + } + } + sections +} + +fn build_segments(macho: &MachO) -> Vec { + macho + .segments + .iter() + .map(|seg| { + let name = seg + .name() + .ok() + .map(|n| n.to_string()); + let initprot = seg.initprot; + let permissions = SectionPermissions { + read: (initprot & VM_PROT_READ) != 0, + write: (initprot & VM_PROT_WRITE) != 0, + execute: (initprot & VM_PROT_EXECUTE) + != 0, + }; + + SegmentInfo { + name, + virtual_address: seg.vmaddr, + virtual_size: seg.vmsize, + file_offset: seg.fileoff, + file_size: seg.filesize, + permissions, + } + }) + .collect() +} + +fn build_macho_info( + macho: &MachO, + is_universal: bool, +) -> MachOInfo { + let file_type = + file_type_name(macho.header.filetype); + let cpu_subtype = cpu_subtype_name( + macho.header.cputype, + macho.header.cpusubtype, + ); + + let mut has_code_signature = false; + let mut has_function_starts = false; + let mut min_os_version: Option = None; + let mut sdk_version: Option = None; + + for lc in &macho.load_commands { + match &lc.command { + CommandVariant::CodeSignature(_) => { + has_code_signature = true; + } + CommandVariant::FunctionStarts(_) => { + has_function_starts = true; + } + CommandVariant::VersionMinMacosx(ver) => { + min_os_version = Some(format!( + "{}.{}.{}", + ver.version >> 16, + (ver.version >> 8) & 0xFF, + ver.version & 0xFF, + )); + sdk_version = Some(format!( + "{}.{}.{}", + ver.sdk >> 16, + (ver.sdk >> 8) & 0xFF, + ver.sdk & 0xFF, + )); + } + CommandVariant::VersionMinIphoneos(ver) => { + if min_os_version.is_none() { + min_os_version = Some(format!( + "iOS {}.{}.{}", + ver.version >> 16, + (ver.version >> 8) & 0xFF, + ver.version & 0xFF, + )); + sdk_version = Some(format!( + "{}.{}.{}", + ver.sdk >> 16, + (ver.sdk >> 8) & 0xFF, + ver.sdk & 0xFF, + )); + } + } + CommandVariant::BuildVersion(bv) => { + if min_os_version.is_none() { + min_os_version = Some(format!( + "{}.{}.{}", + bv.minos >> 16, + (bv.minos >> 8) & 0xFF, + bv.minos & 0xFF, + )); + sdk_version = Some(format!( + "{}.{}.{}", + bv.sdk >> 16, + (bv.sdk >> 8) & 0xFF, + bv.sdk & 0xFF, + )); + } + } + _ => {} + } + } + + let dylibs = macho + .libs + .iter() + .filter(|lib| !lib.is_empty()) + .map(|lib| lib.to_string()) + .collect(); + + MachOInfo { + file_type, + cpu_subtype, + is_universal, + has_code_signature, + min_os_version, + sdk_version, + dylibs, + has_function_starts, + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs new file mode 100644 index 0000000..a31a65f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs @@ -0,0 +1,335 @@ +// ©AngelaMos | 2026 +// mod.rs + +mod elf; +mod macho; +mod pe; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::error::EngineError; +use crate::types::{ + Architecture, BinaryFormat, Endianness, SectionPermissions, +}; + +pub const SUSPICIOUS_SECTION_NAMES: &[(&str, &str)] = &[ + ("UPX0", "UPX packer"), + ("UPX1", "UPX packer"), + ("UPX2", "UPX packer"), + (".nsp0", "NSPack"), + (".nsp1", "NSPack"), + (".nsp2", "NSPack"), + (".aspack", "ASPack"), + (".adata", "ASPack"), + (".MPress1", "MPress"), + (".MPress2", "MPress"), + (".themida", "Themida"), + (".vmp0", "VMProtect"), + (".vmp1", "VMProtect"), + (".enigma1", "Enigma"), + (".enigma2", "Enigma"), +]; + +const VIRTUAL_RAW_RATIO_THRESHOLD: f64 = 10.0; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FormatResult { + pub format: BinaryFormat, + pub architecture: Architecture, + pub bits: u8, + pub endianness: Endianness, + pub entry_point: u64, + pub is_stripped: bool, + pub is_pie: bool, + pub has_debug_info: bool, + pub sections: Vec, + pub segments: Vec, + pub anomalies: Vec, + pub pe_info: Option, + pub elf_info: Option, + pub macho_info: Option, + #[serde(default)] + pub function_hints: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SectionInfo { + pub name: String, + pub virtual_address: u64, + pub virtual_size: u64, + pub raw_offset: u64, + pub raw_size: u64, + pub permissions: SectionPermissions, + pub sha256: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SegmentInfo { + pub name: Option, + pub virtual_address: u64, + pub virtual_size: u64, + pub file_offset: u64, + pub file_size: u64, + pub permissions: SectionPermissions, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FormatAnomaly { + EntryPointOutsideText { + ep: u64, + text_range: (u64, u64), + }, + EntryPointInLastSection { + ep: u64, + section: String, + }, + EntryPointOutsideSections { + ep: u64, + }, + RwxSection { + name: String, + }, + EmptySectionName { + index: usize, + }, + StrippedBinary, + SuspiciousSectionName { + name: String, + reason: String, + }, + ZeroSizeCodeSection { + name: String, + }, + VirtualRawSizeMismatch { + name: String, + virtual_size: u64, + raw_size: u64, + ratio: f64, + }, + OverlayData { + offset: u64, + size: u64, + }, + TlsCallbacksPresent { + count: usize, + }, + NoImportTable, + SuspiciousTimestamp { + value: u32, + reason: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PeInfo { + pub image_base: u64, + pub subsystem: String, + pub dll_characteristics: PeDllCharacteristics, + pub timestamp: u32, + pub linker_version: String, + pub tls_callback_count: usize, + pub has_overlay: bool, + pub overlay_size: u64, + pub rich_header_present: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PeDllCharacteristics { + pub aslr: bool, + pub dep: bool, + pub cfg: bool, + pub no_seh: bool, + pub force_integrity: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ElfInfo { + pub os_abi: String, + pub elf_type: String, + pub interpreter: Option, + pub gnu_relro: bool, + pub bind_now: bool, + pub stack_executable: bool, + pub needed_libraries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MachOInfo { + pub file_type: String, + pub cpu_subtype: String, + pub is_universal: bool, + pub has_code_signature: bool, + pub min_os_version: Option, + pub sdk_version: Option, + pub dylibs: Vec, + pub has_function_starts: bool, +} + +pub fn parse_format( + data: &[u8], +) -> Result { + let object = + goblin::Object::parse(data).map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + + match &object { + goblin::Object::Elf(elf_obj) => { + elf::parse_elf(elf_obj, data) + } + goblin::Object::PE(pe_obj) => { + pe::parse_pe(pe_obj, data) + } + goblin::Object::Mach(mach_obj) => { + macho::parse_macho(mach_obj, data) + } + _ => Err(EngineError::UnsupportedFormat { + format: "unknown".into(), + }), + } +} + +fn compute_section_hash( + data: &[u8], + offset: u64, + size: u64, +) -> String { + if size == 0 { + return String::new(); + } + let start = offset as usize; + let end = start.saturating_add(size as usize); + if start >= data.len() || end > data.len() { + return String::new(); + } + let hash = Sha256::digest(&data[start..end]); + format!("{hash:x}") +} + +fn check_suspicious_name(name: &str) -> Option { + for &(suspicious, reason) in SUSPICIOUS_SECTION_NAMES { + if name == suspicious { + return Some(reason.into()); + } + } + None +} + +fn detect_common_anomalies( + sections: &[SectionInfo], + entry_point: u64, + is_stripped: bool, +) -> Vec { + let mut anomalies = Vec::new(); + + let text_section = + sections.iter().find(|s| s.name == ".text"); + if let Some(text) = text_section { + let text_end = + text.virtual_address + text.virtual_size; + if entry_point != 0 + && (entry_point < text.virtual_address + || entry_point >= text_end) + { + anomalies.push( + FormatAnomaly::EntryPointOutsideText { + ep: entry_point, + text_range: ( + text.virtual_address, + text_end, + ), + }, + ); + } + } + + if let Some(last) = sections.last() { + let last_end = + last.virtual_address + last.virtual_size; + if entry_point >= last.virtual_address + && entry_point < last_end + { + anomalies.push( + FormatAnomaly::EntryPointInLastSection { + ep: entry_point, + section: last.name.clone(), + }, + ); + } + } + + let ep_in_any = sections.iter().any(|s| { + entry_point >= s.virtual_address + && entry_point + < s.virtual_address + s.virtual_size + }); + if !ep_in_any && entry_point != 0 { + anomalies.push( + FormatAnomaly::EntryPointOutsideSections { + ep: entry_point, + }, + ); + } + + for (idx, section) in sections.iter().enumerate() { + if section.permissions.is_rwx() { + anomalies.push(FormatAnomaly::RwxSection { + name: section.name.clone(), + }); + } + + if section.name.is_empty() { + anomalies.push( + FormatAnomaly::EmptySectionName { + index: idx, + }, + ); + } + + if let Some(reason) = + check_suspicious_name(§ion.name) + { + anomalies.push( + FormatAnomaly::SuspiciousSectionName { + name: section.name.clone(), + reason, + }, + ); + } + + if section.permissions.execute + && section.virtual_size == 0 + { + anomalies.push( + FormatAnomaly::ZeroSizeCodeSection { + name: section.name.clone(), + }, + ); + } + + if section.raw_size > 0 { + let ratio = section.virtual_size as f64 + / section.raw_size as f64; + if ratio > VIRTUAL_RAW_RATIO_THRESHOLD { + anomalies.push( + FormatAnomaly::VirtualRawSizeMismatch { + name: section.name.clone(), + virtual_size: section + .virtual_size, + raw_size: section.raw_size, + ratio, + }, + ); + } + } + } + + if is_stripped { + anomalies.push(FormatAnomaly::StrippedBinary); + } + + anomalies +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs new file mode 100644 index 0000000..67d2c28 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs @@ -0,0 +1,332 @@ +// ©AngelaMos | 2026 +// pe.rs + +use goblin::pe::PE; + +use super::{ + compute_section_hash, detect_common_anomalies, + FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo, + SectionInfo, SegmentInfo, +}; +use crate::error::EngineError; +use crate::types::{ + Architecture, BinaryFormat, Endianness, SectionPermissions, +}; + +const COFF_MACHINE_I386: u16 = 0x14c; +const COFF_MACHINE_AMD64: u16 = 0x8664; +const COFF_MACHINE_ARM: u16 = 0x1c0; +const COFF_MACHINE_ARMNT: u16 = 0x1c4; +const COFF_MACHINE_ARM64: u16 = 0xaa64; + +const IMAGE_SCN_MEM_READ: u32 = 0x4000_0000; +const IMAGE_SCN_MEM_WRITE: u32 = 0x8000_0000; +const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000; + +const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040; +const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = + 0x0080; +const IMAGE_DLLCHARACTERISTICS_NX_COMPAT: u16 = 0x0100; +const IMAGE_DLLCHARACTERISTICS_NO_SEH: u16 = 0x0400; +const IMAGE_DLLCHARACTERISTICS_GUARD_CF: u16 = 0x4000; + +const IMAGE_SUBSYSTEM_UNKNOWN: u16 = 0; +const IMAGE_SUBSYSTEM_NATIVE: u16 = 1; +const IMAGE_SUBSYSTEM_WINDOWS_GUI: u16 = 2; +const IMAGE_SUBSYSTEM_WINDOWS_CUI: u16 = 3; +const IMAGE_SUBSYSTEM_POSIX_CUI: u16 = 7; +const IMAGE_SUBSYSTEM_WINDOWS_CE_GUI: u16 = 9; +const IMAGE_SUBSYSTEM_EFI_APPLICATION: u16 = 10; +const IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER: u16 = 11; +const IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER: u16 = 12; +const IMAGE_SUBSYSTEM_XBOX: u16 = 14; + +const PE_TIMESTAMP_MIN_VALID: u32 = 631_152_000; +const PE_TIMESTAMP_MAX_VALID: u32 = 4_102_444_800; + +const RICH_SIGNATURE: &[u8] = b"Rich"; + +pub fn parse_pe( + pe: &PE, + data: &[u8], +) -> Result { + let architecture = map_architecture( + pe.header.coff_header.machine, + ); + let bits = if pe.is_64 { 64 } else { 32 }; + let endianness = Endianness::Little; + let entry_point = pe.entry as u64; + + let is_stripped = pe.debug_data.is_none(); + let optional = pe.header.optional_header.as_ref(); + let dll_chars = optional.map_or(0, |oh| { + oh.windows_fields.dll_characteristics + }); + let is_pie = (dll_chars + & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) + != 0; + let has_debug_info = pe.debug_data.is_some(); + + let sections = build_sections(pe, data); + let segments = build_segments(pe); + + let timestamp = + pe.header.coff_header.time_date_stamp; + let image_base = optional + .map_or(0, |oh| oh.windows_fields.image_base); + let subsystem_raw = optional + .map_or(0, |oh| oh.windows_fields.subsystem); + let linker_version = optional.map_or_else( + || "0.0".into(), + |oh| { + format!( + "{}.{}", + oh.standard_fields.major_linker_version, + oh.standard_fields.minor_linker_version, + ) + }, + ); + + let has_tls = pe.tls_data.is_some(); + let tls_callback_count = usize::from(has_tls); + + let max_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); + let file_size = data.len() as u64; + let has_overlay = + max_section_end > 0 && max_section_end < file_size; + let overlay_size = if has_overlay { + file_size - max_section_end + } else { + 0 + }; + + let pe_offset = + pe.header.dos_header.pe_pointer as usize; + let rich_header_present = detect_rich_header( + data, + pe_offset, + ); + + let pe_info = PeInfo { + image_base, + subsystem: subsystem_name(subsystem_raw), + dll_characteristics: PeDllCharacteristics { + aslr: (dll_chars + & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) + != 0, + dep: (dll_chars + & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) + != 0, + cfg: (dll_chars + & IMAGE_DLLCHARACTERISTICS_GUARD_CF) + != 0, + no_seh: (dll_chars + & IMAGE_DLLCHARACTERISTICS_NO_SEH) + != 0, + force_integrity: (dll_chars + & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) + != 0, + }, + timestamp, + linker_version, + tls_callback_count, + has_overlay, + overlay_size, + rich_header_present, + }; + + let mut anomalies = detect_common_anomalies( + §ions, + entry_point, + is_stripped, + ); + detect_pe_anomalies( + &mut anomalies, + pe, + timestamp, + has_tls, + has_overlay, + max_section_end, + file_size, + ); + + let function_hints: Vec = pe + .exports + .iter() + .filter(|e| e.rva != 0) + .map(|e| image_base + e.rva as u64) + .filter(|&addr| addr != entry_point) + .collect(); + + Ok(FormatResult { + format: BinaryFormat::Pe, + architecture, + bits, + endianness, + entry_point, + is_stripped, + is_pie, + has_debug_info, + sections, + segments, + anomalies, + pe_info: Some(pe_info), + elf_info: None, + macho_info: None, + function_hints, + }) +} + +fn map_architecture(machine: u16) -> Architecture { + match machine { + COFF_MACHINE_I386 => Architecture::X86, + COFF_MACHINE_AMD64 => Architecture::X86_64, + COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => { + Architecture::Arm + } + COFF_MACHINE_ARM64 => Architecture::Aarch64, + other => { + Architecture::Other(format!( + "pe-machine-{other:#x}" + )) + } + } +} + +fn subsystem_name(subsystem: u16) -> String { + match subsystem { + IMAGE_SUBSYSTEM_UNKNOWN => "Unknown".into(), + IMAGE_SUBSYSTEM_NATIVE => "Native".into(), + IMAGE_SUBSYSTEM_WINDOWS_GUI => "GUI".into(), + IMAGE_SUBSYSTEM_WINDOWS_CUI => "Console".into(), + IMAGE_SUBSYSTEM_POSIX_CUI => "POSIX".into(), + IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => { + "Windows CE".into() + } + IMAGE_SUBSYSTEM_EFI_APPLICATION => { + "EFI Application".into() + } + IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER => { + "EFI Boot Service Driver".into() + } + IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER => { + "EFI Runtime Driver".into() + } + IMAGE_SUBSYSTEM_XBOX => "Xbox".into(), + other => format!("Unknown({other})"), + } +} + +fn build_sections( + pe: &PE, + data: &[u8], +) -> Vec { + pe.sections + .iter() + .map(|section| { + let name = section + .name() + .unwrap_or_default() + .to_string(); + let raw_offset = + section.pointer_to_raw_data as u64; + let raw_size = + section.size_of_raw_data as u64; + let chars = section.characteristics; + let permissions = SectionPermissions { + read: (chars & IMAGE_SCN_MEM_READ) != 0, + write: (chars & IMAGE_SCN_MEM_WRITE) != 0, + execute: (chars & IMAGE_SCN_MEM_EXECUTE) + != 0, + }; + let sha256 = compute_section_hash( + data, raw_offset, raw_size, + ); + + SectionInfo { + name, + virtual_address: section.virtual_address + as u64, + virtual_size: section.virtual_size as u64, + raw_offset, + raw_size, + permissions, + sha256, + } + }) + .collect() +} + +fn build_segments(_pe: &PE) -> Vec { + Vec::new() +} + +fn detect_rich_header( + data: &[u8], + pe_offset: usize, +) -> bool { + let end = pe_offset.min(data.len()); + data[..end] + .windows(RICH_SIGNATURE.len()) + .any(|w| w == RICH_SIGNATURE) +} + +fn detect_pe_anomalies( + anomalies: &mut Vec, + pe: &PE, + timestamp: u32, + has_tls: bool, + has_overlay: bool, + overlay_offset: u64, + file_size: u64, +) { + if timestamp == 0 { + anomalies.push( + FormatAnomaly::SuspiciousTimestamp { + value: timestamp, + reason: "zeroed timestamp".into(), + }, + ); + } else if timestamp < PE_TIMESTAMP_MIN_VALID { + anomalies.push( + FormatAnomaly::SuspiciousTimestamp { + value: timestamp, + reason: "timestamp before 1990".into(), + }, + ); + } else if timestamp > PE_TIMESTAMP_MAX_VALID { + anomalies.push( + FormatAnomaly::SuspiciousTimestamp { + value: timestamp, + reason: "timestamp after 2100".into(), + }, + ); + } + + if has_tls { + anomalies.push( + FormatAnomaly::TlsCallbacksPresent { + count: 1, + }, + ); + } + + if pe.imports.is_empty() { + anomalies.push(FormatAnomaly::NoImportTable); + } + + if has_overlay { + anomalies.push(FormatAnomaly::OverlayData { + offset: overlay_offset, + size: file_size - overlay_offset, + }); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs new file mode 100644 index 0000000..ee0e2f1 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs @@ -0,0 +1,76 @@ +// ©AngelaMos | 2026 +// lib.rs + +pub mod context; +pub mod error; +pub mod formats; +pub mod pass; +pub mod passes; +pub mod types; +pub mod yara; + +use std::sync::Arc; + +use sha2::{Digest, Sha256}; + +use context::{AnalysisContext, BinarySource}; +use error::EngineError; +use pass::{PassManager, PassReport}; +use passes::disasm::DisasmPass; +use passes::entropy::EntropyPass; +use passes::format::FormatPass; +use passes::imports::ImportPass; +use passes::strings::StringPass; +use passes::threat::ThreatPass; + +pub struct AnalysisEngine { + pass_manager: PassManager, +} + +impl AnalysisEngine { + pub fn new() -> Result { + let passes: Vec> = + vec![ + Box::new(FormatPass), + Box::new(ImportPass), + Box::new(StringPass), + Box::new(EntropyPass), + Box::new(DisasmPass), + Box::new(ThreatPass), + ]; + + let pass_manager = PassManager::new(passes); + + Ok(Self { pass_manager }) + } + + pub fn analyze( + &self, + data: &[u8], + file_name: &str, + ) -> (AnalysisContext, PassReport) { + let sha256 = compute_sha256(data); + let file_size = data.len() as u64; + let mut ctx = AnalysisContext::new( + BinarySource::Buffered(Arc::from( + data.to_vec(), + )), + sha256, + file_name.to_string(), + file_size, + ); + let report = + self.pass_manager.run_all(&mut ctx); + (ctx, report) + } +} + +pub fn sha256_hex(data: &[u8]) -> String { + compute_sha256(data) +} + +fn compute_sha256(data: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(data); + format!("{:x}", hasher.finalize()) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs new file mode 100644 index 0000000..98627fb --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs @@ -0,0 +1,386 @@ +// ©AngelaMos | 2026 +// pass.rs + +use std::collections::{HashMap, VecDeque}; +use std::time::Instant; + +use crate::context::AnalysisContext; +use crate::error::EngineError; + +mod private { + pub trait Sealed {} +} + +pub trait AnalysisPass: private::Sealed + Send + Sync { + fn name(&self) -> &'static str; + fn dependencies(&self) -> &[&'static str]; + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError>; +} + +#[derive(Debug, Clone)] +pub struct PassOutcome { + pub name: &'static str, + pub success: bool, + pub duration_ms: u64, + pub error_message: Option, +} + +#[derive(Debug, Clone)] +pub struct PassReport { + pub outcomes: Vec, +} + +impl PassReport { + pub fn all_succeeded(&self) -> bool { + self.outcomes.iter().all(|o| o.success) + } + + pub fn failed_passes(&self) -> Vec<&PassOutcome> { + self.outcomes + .iter() + .filter(|o| !o.success) + .collect() + } +} + +pub struct PassManager { + passes: Vec>, + order: Vec, +} + +impl PassManager { + pub fn new( + passes: Vec>, + ) -> Self { + let order = topological_order(&passes); + Self { passes, order } + } + + pub fn run_all( + &self, + ctx: &mut AnalysisContext, + ) -> PassReport { + let mut outcomes = Vec::with_capacity(self.passes.len()); + + for &idx in &self.order { + let pass = &self.passes[idx]; + let start = Instant::now(); + let result = pass.run(ctx); + let duration_ms = + start.elapsed().as_millis() as u64; + + let outcome = match result { + Ok(()) => { + tracing::info!( + pass = pass.name(), + duration_ms, + "pass completed" + ); + PassOutcome { + name: pass.name(), + success: true, + duration_ms, + error_message: None, + } + } + Err(e) => { + tracing::error!( + pass = pass.name(), + error = %e, + duration_ms, + "pass failed" + ); + PassOutcome { + name: pass.name(), + success: false, + duration_ms, + error_message: Some(e.to_string()), + } + } + }; + + outcomes.push(outcome); + } + + PassReport { outcomes } + } +} + +fn topological_order( + passes: &[Box], +) -> Vec { + let name_to_idx: HashMap<&str, usize> = passes + .iter() + .enumerate() + .map(|(i, p)| (p.name(), i)) + .collect(); + + let n = passes.len(); + let mut in_degree = vec![0usize; n]; + let mut adjacency: Vec> = vec![vec![]; n]; + + for (idx, pass) in passes.iter().enumerate() { + for dep_name in pass.dependencies() { + if let Some(&dep_idx) = name_to_idx.get(dep_name) + { + adjacency[dep_idx].push(idx); + in_degree[idx] += 1; + } + } + } + + let mut queue: VecDeque = in_degree + .iter() + .enumerate() + .filter(|&(_, deg)| *deg == 0) + .map(|(i, _)| i) + .collect(); + + let mut order = Vec::with_capacity(n); + + while let Some(node) = queue.pop_front() { + order.push(node); + for &neighbor in &adjacency[node] { + in_degree[neighbor] -= 1; + if in_degree[neighbor] == 0 { + queue.push_back(neighbor); + } + } + } + + assert_eq!( + order.len(), + n, + "cycle detected in pass dependencies — this is a programmer error" + ); + + order +} + +pub(crate) use private::Sealed; + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + struct MockPass { + name: &'static str, + deps: Vec<&'static str>, + log: Arc>>, + should_fail: bool, + } + + impl Sealed for MockPass {} + + impl AnalysisPass for MockPass { + fn name(&self) -> &'static str { + self.name + } + + fn dependencies(&self) -> &[&'static str] { + &self.deps + } + + fn run( + &self, + _ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + self.log.lock().unwrap().push(self.name); + if self.should_fail { + return Err(EngineError::PassFailed { + pass: self.name, + source: "mock failure".into(), + }); + } + Ok(()) + } + } + + fn make_ctx() -> AnalysisContext { + AnalysisContext::new( + crate::context::BinarySource::Buffered( + Arc::from(vec![0u8; 4]), + ), + "deadbeef".into(), + "test.bin".into(), + 4, + ) + } + + #[test] + fn topological_sort_respects_dependencies() { + let log = Arc::new(Mutex::new(Vec::new())); + + let passes: Vec> = vec![ + Box::new(MockPass { + name: "c", + deps: vec!["b"], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "a", + deps: vec![], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "b", + deps: vec!["a"], + log: Arc::clone(&log), + should_fail: false, + }), + ]; + + let manager = PassManager::new(passes); + let mut ctx = make_ctx(); + let report = manager.run_all(&mut ctx); + + let execution_order = log.lock().unwrap().clone(); + assert_eq!(execution_order, vec!["a", "b", "c"]); + assert!(report.all_succeeded()); + assert_eq!(report.outcomes.len(), 3); + } + + #[test] + fn continues_on_failure() { + let log = Arc::new(Mutex::new(Vec::new())); + + let passes: Vec> = vec![ + Box::new(MockPass { + name: "first", + deps: vec![], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "second", + deps: vec![], + log: Arc::clone(&log), + should_fail: true, + }), + Box::new(MockPass { + name: "third", + deps: vec![], + log: Arc::clone(&log), + should_fail: false, + }), + ]; + + let manager = PassManager::new(passes); + let mut ctx = make_ctx(); + let report = manager.run_all(&mut ctx); + + let execution_order = log.lock().unwrap().clone(); + assert_eq!( + execution_order, + vec!["first", "second", "third"] + ); + assert!(!report.all_succeeded()); + assert_eq!(report.failed_passes().len(), 1); + assert_eq!( + report.failed_passes()[0].name, + "second" + ); + } + + #[test] + fn diamond_dependency_ordering() { + let log = Arc::new(Mutex::new(Vec::new())); + + let passes: Vec> = vec![ + Box::new(MockPass { + name: "score", + deps: vec!["imports", "entropy"], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "entropy", + deps: vec!["format"], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "format", + deps: vec![], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "imports", + deps: vec!["format"], + log: Arc::clone(&log), + should_fail: false, + }), + ]; + + let manager = PassManager::new(passes); + let mut ctx = make_ctx(); + manager.run_all(&mut ctx); + + let order = log.lock().unwrap().clone(); + let format_pos = + order.iter().position(|&n| n == "format").unwrap(); + let imports_pos = + order.iter().position(|&n| n == "imports").unwrap(); + let entropy_pos = + order.iter().position(|&n| n == "entropy").unwrap(); + let score_pos = + order.iter().position(|&n| n == "score").unwrap(); + + assert!(format_pos < imports_pos); + assert!(format_pos < entropy_pos); + assert!(imports_pos < score_pos); + assert!(entropy_pos < score_pos); + } + + #[test] + #[should_panic(expected = "cycle detected")] + fn detects_cycle() { + let log = Arc::new(Mutex::new(Vec::new())); + + let passes: Vec> = vec![ + Box::new(MockPass { + name: "a", + deps: vec!["b"], + log: Arc::clone(&log), + should_fail: false, + }), + Box::new(MockPass { + name: "b", + deps: vec!["a"], + log: Arc::clone(&log), + should_fail: false, + }), + ]; + + let _manager = PassManager::new(passes); + } + + #[test] + fn reports_duration() { + let log = Arc::new(Mutex::new(Vec::new())); + + let passes: Vec> = vec![ + Box::new(MockPass { + name: "fast", + deps: vec![], + log: Arc::clone(&log), + should_fail: false, + }), + ]; + + let manager = PassManager::new(passes); + let mut ctx = make_ctx(); + let report = manager.run_all(&mut ctx); + + assert_eq!(report.outcomes.len(), 1); + assert_eq!(report.outcomes[0].name, "fast"); + assert!(report.outcomes[0].success); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs new file mode 100644 index 0000000..790f205 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs @@ -0,0 +1,948 @@ +// ©AngelaMos | 2026 +// disasm.rs + +use std::collections::{ + BTreeMap, HashMap, HashSet, VecDeque, +}; + +use iced_x86::{ + Decoder, DecoderOptions, FlowControl, Formatter, + Instruction, IntelFormatter, +}; +use serde::{Deserialize, Serialize}; + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::formats::SectionInfo; +use crate::pass::{AnalysisPass, Sealed}; +use crate::types::{ + Architecture, CfgEdgeType, FlowControlType, +}; + +const MAX_FUNCTIONS: usize = 1000; +const MAX_INSTRUCTIONS: usize = 50_000; +const CFG_INSTRUCTION_LIMIT: usize = 500; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisassemblyResult { + pub functions: Vec, + pub total_instructions: usize, + pub total_functions: usize, + pub architecture_bits: u8, + pub entry_function_address: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionInfo { + pub address: u64, + pub name: Option, + pub size: u64, + pub instruction_count: usize, + pub basic_blocks: Vec, + pub is_entry_point: bool, + pub cfg: FunctionCfg, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BasicBlockInfo { + pub start_address: u64, + pub end_address: u64, + pub instruction_count: usize, + pub instructions: Vec, + pub successors: Vec, + pub predecessors: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstructionInfo { + pub address: u64, + pub bytes: Vec, + pub mnemonic: String, + pub operands: String, + pub size: u8, + pub flow_control: FlowControlType, +} + +#[derive( + Debug, Clone, Default, Serialize, Deserialize, +)] +pub struct FunctionCfg { + pub nodes: Vec, + pub edges: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CfgNode { + pub id: u64, + pub label: String, + pub instruction_count: usize, + pub instructions_preview: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CfgEdge { + pub from: u64, + pub to: u64, + pub edge_type: CfgEdgeType, +} + +pub struct DisasmPass; + +impl Sealed for DisasmPass {} + +impl AnalysisPass for DisasmPass { + fn name(&self) -> &'static str { + "disasm" + } + + fn dependencies(&self) -> &[&'static str] { + &["format"] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let format_result = ctx + .format_result + .as_ref() + .ok_or_else(|| EngineError::MissingDependency { + pass: "disasm".into(), + dependency: "format".into(), + })?; + + let arch = &format_result.architecture; + let bits = match arch { + Architecture::X86 => 32u32, + Architecture::X86_64 => 64, + _ => { + ctx.disassembly_result = + Some(empty_result( + format_result.bits, + format_result.entry_point, + )); + return Ok(()); + } + }; + + let data = ctx.data(); + let sections = &format_result.sections; + let entry_point = format_result.entry_point; + + let mut seeds = vec![entry_point]; + seeds.extend_from_slice( + &format_result.function_hints, + ); + + let result = disassemble( + data, + sections, + bits, + entry_point, + &seeds, + ); + ctx.disassembly_result = Some(result); + Ok(()) + } +} + +fn empty_result( + bits: u8, + entry_point: u64, +) -> DisassemblyResult { + DisassemblyResult { + functions: Vec::new(), + total_instructions: 0, + total_functions: 0, + architecture_bits: bits, + entry_function_address: entry_point, + } +} + +fn disassemble( + data: &[u8], + sections: &[SectionInfo], + bits: u32, + entry_point: u64, + seeds: &[u64], +) -> DisassemblyResult { + let exec_sections: Vec<&SectionInfo> = sections + .iter() + .filter(|s| s.permissions.execute && s.raw_size > 0) + .collect(); + + let mut functions = Vec::new(); + let mut visited_functions = HashSet::new(); + let mut total_instructions = 0; + let mut function_queue: VecDeque = + seeds.iter().copied().collect(); + + while let Some(func_addr) = function_queue.pop_front() + { + if functions.len() >= MAX_FUNCTIONS + || total_instructions >= MAX_INSTRUCTIONS + { + break; + } + if !visited_functions.insert(func_addr) { + continue; + } + if vaddr_to_offset(sections, func_addr).is_none() + { + continue; + } + + let (func_info, discovered_calls) = + disassemble_function( + data, + sections, + &exec_sections, + bits, + func_addr, + func_addr == entry_point, + MAX_INSTRUCTIONS - total_instructions, + ); + + total_instructions += func_info.instruction_count; + functions.push(func_info); + + for call_target in discovered_calls { + if !visited_functions.contains(&call_target) { + function_queue.push_back(call_target); + } + } + } + + let total_functions = functions.len(); + + DisassemblyResult { + functions, + total_instructions, + total_functions, + architecture_bits: bits as u8, + entry_function_address: entry_point, + } +} + +fn disassemble_function( + data: &[u8], + all_sections: &[SectionInfo], + exec_sections: &[&SectionInfo], + bits: u32, + func_addr: u64, + is_entry_point: bool, + instruction_budget: usize, +) -> (FunctionInfo, Vec) { + let mut decoded: BTreeMap = + BTreeMap::new(); + let mut block_leaders: HashSet = HashSet::new(); + let mut worklist: VecDeque = VecDeque::new(); + let mut visited: HashSet = HashSet::new(); + let mut discovered_calls: Vec = Vec::new(); + let mut formatter = IntelFormatter::new(); + + block_leaders.insert(func_addr); + worklist.push_back(func_addr); + + while let Some(addr) = worklist.pop_front() { + if !visited.insert(addr) { + continue; + } + if decoded.len() >= instruction_budget { + break; + } + + let offset = match vaddr_to_offset( + all_sections, + addr, + ) { + Some(o) => o as usize, + None => continue, + }; + + if !is_in_exec_section(exec_sections, addr) { + continue; + } + + let remaining = data.len().saturating_sub(offset); + if remaining == 0 { + continue; + } + + let slice = &data[offset..]; + let mut decoder = Decoder::with_ip( + bits, + slice, + addr, + DecoderOptions::NONE, + ); + let mut instr = Instruction::default(); + + while decoder.can_decode() + && decoded.len() < instruction_budget + { + decoder.decode_out(&mut instr); + let ip = instr.ip(); + + if ip != addr && visited.contains(&ip) { + break; + } + + if ip != addr + && block_leaders.contains(&ip) + { + break; + } + + let fc = instr.flow_control(); + let mnemonic = format!("{:?}", instr.mnemonic()) + .to_ascii_lowercase(); + + let mut operands_str = String::new(); + formatter + .format(&instr, &mut operands_str); + let operands = operands_str + .split_once(' ') + .map_or(String::new(), |(_, ops)| { + ops.to_string() + }); + + let instr_bytes = &data + [offset + (ip - addr) as usize + ..offset + + (ip - addr) as usize + + instr.len()]; + + let flow_type = map_flow_control(fc); + + decoded.insert( + ip, + DecodedInstruction { + info: InstructionInfo { + address: ip, + bytes: instr_bytes.to_vec(), + mnemonic, + operands, + size: instr.len() as u8, + flow_control: flow_type, + }, + next_ip: instr.next_ip(), + branch_target: None, + fallthrough: None, + }, + ); + + match fc { + FlowControl::ConditionalBranch => { + let target = + instr.near_branch_target(); + let fall = instr.next_ip(); + + if let Some(di) = + decoded.get_mut(&ip) + { + di.branch_target = Some(target); + di.fallthrough = Some(fall); + } + + block_leaders.insert(target); + block_leaders.insert(fall); + worklist.push_back(target); + worklist.push_back(fall); + break; + } + FlowControl::UnconditionalBranch => { + let target = + instr.near_branch_target(); + if let Some(di) = + decoded.get_mut(&ip) + { + di.branch_target = Some(target); + } + block_leaders.insert(target); + worklist.push_back(target); + break; + } + FlowControl::Return + | FlowControl::Interrupt + | FlowControl::IndirectBranch + | FlowControl::Exception => { + break; + } + FlowControl::Call => { + let target = + instr.near_branch_target(); + if target != 0 { + discovered_calls.push(target); + } + } + FlowControl::IndirectCall => {} + FlowControl::Next + | FlowControl::XbeginXabortXend => {} + } + } + } + + let basic_blocks = build_basic_blocks( + &decoded, + &block_leaders, + ); + let instruction_count: usize = basic_blocks + .iter() + .map(|bb| bb.instruction_count) + .sum(); + + let size = if let (Some(first), Some(last)) = ( + decoded.keys().next(), + decoded.keys().next_back(), + ) { + if let Some(last_instr) = decoded.get(last) { + last_instr.info.address + + last_instr.info.size as u64 + - first + } else { + 0 + } + } else { + 0 + }; + + let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT + { + build_cfg(&basic_blocks) + } else { + FunctionCfg::default() + }; + + let func = FunctionInfo { + address: func_addr, + name: None, + size, + instruction_count, + basic_blocks, + is_entry_point, + cfg, + }; + + (func, discovered_calls) +} + +struct DecodedInstruction { + info: InstructionInfo, + next_ip: u64, + branch_target: Option, + fallthrough: Option, +} + +fn build_basic_blocks( + decoded: &BTreeMap, + leaders: &HashSet, +) -> Vec { + if decoded.is_empty() { + return Vec::new(); + } + + let mut blocks: Vec = Vec::new(); + let mut current_instrs: Vec = + Vec::new(); + let mut block_start: Option = None; + + for (&addr, di) in decoded { + if leaders.contains(&addr) + && !current_instrs.is_empty() + { + let bb = finalize_block( + ¤t_instrs, + block_start.unwrap_or(addr), + decoded, + leaders, + ); + blocks.push(bb); + current_instrs.clear(); + block_start = None; + } + + if block_start.is_none() { + block_start = Some(addr); + } + current_instrs.push(di.info.clone()); + + let is_terminator = matches!( + di.info.flow_control, + FlowControlType::Branch + | FlowControlType::ConditionalBranch + | FlowControlType::Return + | FlowControlType::Interrupt + ); + if is_terminator { + let bb = finalize_block( + ¤t_instrs, + block_start.unwrap_or(addr), + decoded, + leaders, + ); + blocks.push(bb); + current_instrs.clear(); + block_start = None; + } + } + + if !current_instrs.is_empty() { + if let Some(start) = block_start { + let bb = finalize_block( + ¤t_instrs, + start, + decoded, + leaders, + ); + blocks.push(bb); + } + } + + let block_starts: HashSet = + blocks.iter().map(|b| b.start_address).collect(); + + for block in &mut blocks { + block + .successors + .retain(|s| block_starts.contains(s)); + } + + let predecessor_map: HashMap> = { + let mut map: HashMap> = + HashMap::new(); + for block in &blocks { + for &succ in &block.successors { + map.entry(succ) + .or_default() + .push(block.start_address); + } + } + map + }; + + for block in &mut blocks { + block.predecessors = predecessor_map + .get(&block.start_address) + .cloned() + .unwrap_or_default(); + } + + blocks +} + +fn finalize_block( + instructions: &[InstructionInfo], + start: u64, + decoded: &BTreeMap, + leaders: &HashSet, +) -> BasicBlockInfo { + let last = instructions.last().unwrap(); + let end_address = + last.address + last.size as u64 - 1; + + let mut successors = Vec::new(); + let last_addr = last.address; + if let Some(di) = decoded.get(&last_addr) { + if let Some(target) = di.branch_target { + successors.push(target); + } + if let Some(fall) = di.fallthrough { + successors.push(fall); + } else if !matches!( + di.info.flow_control, + FlowControlType::Branch + | FlowControlType::Return + | FlowControlType::Interrupt + ) { + let next = di.next_ip; + if leaders.contains(&next) + || decoded.contains_key(&next) + { + successors.push(next); + } + } + } + + BasicBlockInfo { + start_address: start, + end_address, + instruction_count: instructions.len(), + instructions: instructions.to_vec(), + successors, + predecessors: Vec::new(), + } +} + +fn build_cfg( + blocks: &[BasicBlockInfo], +) -> FunctionCfg { + let mut nodes = Vec::new(); + let mut edges = Vec::new(); + + for block in blocks { + let preview = if block.instructions.is_empty() { + String::new() + } else if block.instructions.len() == 1 { + block.instructions[0].mnemonic.clone() + } else { + format!( + "{} ... {}", + block.instructions[0].mnemonic, + block.instructions.last().unwrap().mnemonic + ) + }; + + nodes.push(CfgNode { + id: block.start_address, + label: format!( + "0x{:x}", + block.start_address + ), + instruction_count: block.instruction_count, + instructions_preview: preview, + }); + + let last_instr = block.instructions.last(); + for &succ in &block.successors { + let edge_type = + if let Some(last) = last_instr { + match last.flow_control { + FlowControlType::ConditionalBranch => { + if succ + == block + .successors + .first() + .copied() + .unwrap_or(0) + { + CfgEdgeType::ConditionalTrue + } else { + CfgEdgeType::ConditionalFalse + } + } + FlowControlType::Branch => { + CfgEdgeType::Unconditional + } + _ => CfgEdgeType::Fallthrough, + } + } else { + CfgEdgeType::Fallthrough + }; + + edges.push(CfgEdge { + from: block.start_address, + to: succ, + edge_type, + }); + } + } + + FunctionCfg { nodes, edges } +} + +fn vaddr_to_offset( + sections: &[SectionInfo], + vaddr: u64, +) -> Option { + sections.iter().find_map(|s| { + if s.raw_size > 0 + && vaddr >= s.virtual_address + && vaddr + < s.virtual_address + s.virtual_size + { + Some( + s.raw_offset + + (vaddr - s.virtual_address), + ) + } else { + None + } + }) +} + +fn is_in_exec_section( + exec_sections: &[&SectionInfo], + vaddr: u64, +) -> bool { + exec_sections.iter().any(|s| { + vaddr >= s.virtual_address + && vaddr + < s.virtual_address + s.virtual_size + }) +} + +fn map_flow_control( + fc: FlowControl, +) -> FlowControlType { + match fc { + FlowControl::Next + | FlowControl::XbeginXabortXend => { + FlowControlType::Next + } + FlowControl::UnconditionalBranch + | FlowControl::IndirectBranch => { + FlowControlType::Branch + } + FlowControl::ConditionalBranch => { + FlowControlType::ConditionalBranch + } + FlowControl::Call + | FlowControl::IndirectCall => { + FlowControlType::Call + } + FlowControl::Return => FlowControlType::Return, + FlowControl::Interrupt + | FlowControl::Exception => { + FlowControlType::Interrupt + } + } +} + +pub fn disassemble_code( + code: &[u8], + base_addr: u64, + bits: u32, +) -> Vec { + let mut decoder = Decoder::with_ip( + bits, + code, + base_addr, + DecoderOptions::NONE, + ); + let mut formatter = IntelFormatter::new(); + let mut instr = Instruction::default(); + let mut result = Vec::new(); + + while decoder.can_decode() { + decoder.decode_out(&mut instr); + let mnemonic = format!( + "{:?}", + instr.mnemonic() + ) + .to_ascii_lowercase(); + + let mut full = String::new(); + formatter.format(&instr, &mut full); + let operands = full + .split_once(' ') + .map_or(String::new(), |(_, ops)| { + ops.to_string() + }); + + let start = + (instr.ip() - base_addr) as usize; + let bytes = + code[start..start + instr.len()].to_vec(); + + result.push(InstructionInfo { + address: instr.ip(), + bytes, + mnemonic, + operands, + size: instr.len() as u8, + flow_control: map_flow_control( + instr.flow_control(), + ), + }); + } + + result +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::context::BinarySource; + use crate::types::SectionPermissions; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + fn make_ctx(data: Vec) -> AnalysisContext { + let size = data.len() as u64; + AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ) + } + + #[test] + fn disassemble_simple_function() { + let code: &[u8] = &[ + 0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, + 0x5D, 0xC3, + ]; + let instrs = + disassemble_code(code, 0x1000, 64); + assert_eq!(instrs.len(), 5); + assert_eq!(instrs[0].mnemonic, "push"); + assert_eq!(instrs[4].mnemonic, "ret"); + assert_eq!( + instrs[4].flow_control, + FlowControlType::Return + ); + } + + #[test] + fn basic_block_split_on_branch() { + let code: &[u8] = &[ + 0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, + 0x31, 0xC9, 0xC3, + ]; + + let sections = vec![SectionInfo { + name: ".text".into(), + virtual_address: 0x1000, + virtual_size: code.len() as u64, + raw_offset: 0, + raw_size: code.len() as u64, + permissions: SectionPermissions { + read: true, + write: false, + execute: true, + }, + sha256: String::new(), + }]; + + let result = disassemble( + code, + §ions, + 64, + 0x1000, + &[0x1000], + ); + assert!(!result.functions.is_empty()); + let func = &result.functions[0]; + assert!( + func.basic_blocks.len() >= 2, + "conditional branch should create multiple blocks, got {}", + func.basic_blocks.len() + ); + } + + #[test] + fn cfg_edges_conditional() { + let code: &[u8] = &[ + 0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, + 0x31, 0xC9, 0xC3, + ]; + + let sections = vec![SectionInfo { + name: ".text".into(), + virtual_address: 0x1000, + virtual_size: code.len() as u64, + raw_offset: 0, + raw_size: code.len() as u64, + permissions: SectionPermissions { + read: true, + write: false, + execute: true, + }, + sha256: String::new(), + }]; + + let result = disassemble( + code, + §ions, + 64, + 0x1000, + &[0x1000], + ); + let func = &result.functions[0]; + assert!( + !func.cfg.edges.is_empty(), + "CFG should have edges" + ); + assert!( + !func.cfg.nodes.is_empty(), + "CFG should have nodes" + ); + } + + #[test] + fn non_x86_returns_empty() { + let data = vec![0u8; 64]; + let mut ctx = make_ctx(data); + ctx.format_result = + Some(crate::formats::FormatResult { + format: crate::types::BinaryFormat::Elf, + architecture: Architecture::Aarch64, + bits: 64, + endianness: + crate::types::Endianness::Little, + entry_point: 0x1000, + is_stripped: false, + is_pie: false, + has_debug_info: false, + sections: Vec::new(), + segments: Vec::new(), + anomalies: Vec::new(), + pe_info: None, + elf_info: None, + macho_info: None, + function_hints: Vec::new(), + }); + + DisasmPass.run(&mut ctx).unwrap(); + let result = ctx.disassembly_result.unwrap(); + assert!(result.functions.is_empty()); + assert_eq!(result.total_instructions, 0); + } + + #[test] + fn elf_disassembly() { + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + + crate::passes::format::FormatPass + .run(&mut ctx) + .unwrap(); + DisasmPass.run(&mut ctx).unwrap(); + + let result = + ctx.disassembly_result.as_ref().unwrap(); + assert!( + result.total_functions > 0, + "should find at least one function" + ); + assert!(result.total_instructions > 0); + + let entry_func = result.functions.iter().find( + |f| { + f.address + == result.entry_function_address + }, + ); + assert!( + entry_func.is_some() + || !result.functions.is_empty(), + "should have disassembled functions" + ); + } + + #[test] + fn disasm_pass_populates_context() { + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + + crate::passes::format::FormatPass + .run(&mut ctx) + .unwrap(); + assert!(ctx.disassembly_result.is_none()); + + DisasmPass.run(&mut ctx).unwrap(); + assert!(ctx.disassembly_result.is_some()); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs new file mode 100644 index 0000000..423d206 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs @@ -0,0 +1,540 @@ +// ©AngelaMos | 2026 +// entropy.rs + +use serde::{Deserialize, Serialize}; + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::formats::SectionInfo; +use crate::pass::{AnalysisPass, Sealed}; +use crate::types::{ + EntropyClassification, EntropyFlag, +}; + +const PLAINTEXT_MAX: f64 = 3.5; +const NATIVE_CODE_MAX: f64 = 6.0; +const COMPRESSED_MAX: f64 = 7.0; +const PACKED_MAX: f64 = 7.2; + +const HIGH_ENTROPY_THRESHOLD: f64 = 7.0; +const VIRTUAL_RAW_RATIO_THRESHOLD: f64 = 10.0; + +const BYTE_RANGE: usize = 256; + +const STRUCTURAL_INDICATORS_FOR_PACKING: usize = 2; + +const PUSHAD_OPCODE: u8 = 0x60; + +const PACKER_SECTION_NAMES: &[(&str, &str)] = &[ + ("UPX0", "UPX"), + ("UPX1", "UPX"), + ("UPX2", "UPX"), + (".themida", "Themida"), + (".vmp0", "VMProtect"), + (".vmp1", "VMProtect"), + (".vmp2", "VMProtect"), + (".aspack", "ASPack"), + (".adata", "ASPack"), + ("PEC2TO", "PECompact"), + ("PEC2", "PECompact"), + ("pec1", "PECompact"), + (".MPRESS1", "MPRESS"), + (".MPRESS2", "MPRESS"), + (".enigma1", "Enigma"), + (".enigma2", "Enigma"), +]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntropyResult { + pub overall_entropy: f64, + pub sections: Vec, + pub packing_detected: bool, + pub packer_name: Option, + pub packing_indicators: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SectionEntropy { + pub name: String, + pub entropy: f64, + pub size: u64, + pub classification: EntropyClassification, + pub virtual_to_raw_ratio: f64, + pub is_anomalous: bool, + pub flags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PackingIndicator { + pub indicator_type: String, + pub description: String, + pub evidence: String, + pub packer_name: Option, +} + +pub struct EntropyPass; + +impl Sealed for EntropyPass {} + +impl AnalysisPass for EntropyPass { + fn name(&self) -> &'static str { + "entropy" + } + + fn dependencies(&self) -> &[&'static str] { + &["format"] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let format_result = ctx + .format_result + .as_ref() + .ok_or_else(|| EngineError::MissingDependency { + pass: "entropy".into(), + dependency: "format".into(), + })?; + + let data = ctx.data(); + let result = analyze_entropy( + data, + &format_result.sections, + format_result.entry_point, + ); + ctx.entropy_result = Some(result); + Ok(()) + } +} + +fn analyze_entropy( + data: &[u8], + sections: &[SectionInfo], + entry_point: u64, +) -> EntropyResult { + let overall_entropy = shannon_entropy(data); + let mut section_entropies = Vec::new(); + let mut packing_indicators = Vec::new(); + let mut packer_name: Option = None; + let mut structural_count = 0; + + for section in sections { + let section_data = read_section_data( + data, + section.raw_offset, + section.raw_size, + ); + let entropy = if section_data.is_empty() { + 0.0 + } else { + shannon_entropy(section_data) + }; + let classification = classify_entropy(entropy); + let vr_ratio = if section.raw_size > 0 { + section.virtual_size as f64 + / section.raw_size as f64 + } else { + 0.0 + }; + + let mut flags = Vec::new(); + + if entropy > HIGH_ENTROPY_THRESHOLD { + flags.push(EntropyFlag::HighEntropy); + } + if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD { + flags.push(EntropyFlag::HighVirtualToRawRatio); + } + if section.raw_size == 0 + && section.virtual_size > 0 + { + flags.push(EntropyFlag::EmptyRawData); + } + if section.permissions.is_rwx() { + flags.push(EntropyFlag::Rwx); + } + + if let Some(packer) = + detect_packer_by_section(§ion.name) + { + flags.push(EntropyFlag::PackerSectionName); + packing_indicators.push(PackingIndicator { + indicator_type: "section_name".into(), + description: format!( + "Section name matches {packer} packer" + ), + evidence: section.name.clone(), + packer_name: Some(packer.into()), + }); + if packer_name.is_none() { + packer_name = Some(packer.into()); + } + } + + if section.raw_size == 0 + && section.virtual_size > 0 + && section.permissions.execute + { + structural_count += 1; + packing_indicators.push(PackingIndicator { + indicator_type: "structural".into(), + description: + "Empty raw data with executable \ + virtual section" + .into(), + evidence: format!( + "section={} raw=0 virtual={}", + section.name, section.virtual_size + ), + packer_name: None, + }); + } + + if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD { + structural_count += 1; + packing_indicators.push(PackingIndicator { + indicator_type: "structural".into(), + description: + "High virtual to raw size ratio" + .into(), + evidence: format!( + "section={} ratio={vr_ratio:.1}", + section.name + ), + packer_name: None, + }); + } + + let is_anomalous = !flags.is_empty(); + + section_entropies.push(SectionEntropy { + name: section.name.clone(), + entropy, + size: section.raw_size, + classification, + virtual_to_raw_ratio: vr_ratio, + is_anomalous, + flags, + }); + } + + if let Some(ep_section) = find_ep_section( + sections, + entry_point, + ) { + let ep_file_offset = entry_point + .wrapping_sub(ep_section.virtual_address) + .wrapping_add(ep_section.raw_offset); + if let Some(&first_byte) = + data.get(ep_file_offset as usize) + { + if first_byte == PUSHAD_OPCODE { + packing_indicators.push( + PackingIndicator { + indicator_type: "entry_point" + .into(), + description: + "PUSHAD at entry point" + .into(), + evidence: format!( + "byte 0x{PUSHAD_OPCODE:02x} \ + at EP offset 0x{ep_file_offset:x}" + ), + packer_name: None, + }, + ); + } + } + } + + let packing_detected = packer_name.is_some() + || structural_count + >= STRUCTURAL_INDICATORS_FOR_PACKING; + + EntropyResult { + overall_entropy, + sections: section_entropies, + packing_detected, + packer_name, + packing_indicators, + } +} + +fn shannon_entropy(data: &[u8]) -> f64 { + if data.is_empty() { + return 0.0; + } + let mut freq = [0u64; BYTE_RANGE]; + for &byte in data { + freq[byte as usize] += 1; + } + let len = data.len() as f64; + freq.iter() + .filter(|&&c| c > 0) + .map(|&c| { + let p = c as f64 / len; + -p * p.log2() + }) + .sum() +} + +fn classify_entropy( + entropy: f64, +) -> EntropyClassification { + if entropy < PLAINTEXT_MAX { + EntropyClassification::Plaintext + } else if entropy < NATIVE_CODE_MAX { + EntropyClassification::NativeCode + } else if entropy < COMPRESSED_MAX { + EntropyClassification::Compressed + } else if entropy < PACKED_MAX { + EntropyClassification::Packed + } else { + EntropyClassification::Encrypted + } +} + +fn detect_packer_by_section( + name: &str, +) -> Option<&'static str> { + PACKER_SECTION_NAMES + .iter() + .find(|&&(section_name, _)| section_name == name) + .map(|&(_, packer)| packer) +} + +fn read_section_data( + data: &[u8], + offset: u64, + size: u64, +) -> &[u8] { + if size == 0 { + return &[]; + } + let start = offset as usize; + let end = start.saturating_add(size as usize); + if start >= data.len() || end > data.len() { + return &[]; + } + &data[start..end] +} + +fn find_ep_section( + sections: &[SectionInfo], + entry_point: u64, +) -> Option<&SectionInfo> { + sections.iter().find(|s| { + entry_point >= s.virtual_address + && entry_point + < s.virtual_address + s.virtual_size + }) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::context::BinarySource; + use crate::types::SectionPermissions; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + fn make_ctx(data: Vec) -> AnalysisContext { + let size = data.len() as u64; + AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ) + } + + #[test] + fn entropy_all_zeros() { + let data = vec![0u8; 1024]; + assert!(shannon_entropy(&data) < 0.001); + } + + #[test] + fn entropy_uniform_distribution() { + let data: Vec = + (0..=255u8).cycle().take(1024).collect(); + let e = shannon_entropy(&data); + assert!( + (e - 8.0).abs() < 0.01, + "uniform distribution should be ~8.0, got {e}" + ); + } + + #[test] + fn entropy_empty_data() { + assert!( + shannon_entropy(&[]) < 0.001, + "empty data should have zero entropy" + ); + } + + #[test] + fn entropy_classification_thresholds() { + assert_eq!( + classify_entropy(2.0), + EntropyClassification::Plaintext + ); + assert_eq!( + classify_entropy(5.0), + EntropyClassification::NativeCode + ); + assert_eq!( + classify_entropy(6.5), + EntropyClassification::Compressed + ); + assert_eq!( + classify_entropy(7.1), + EntropyClassification::Packed + ); + assert_eq!( + classify_entropy(7.5), + EntropyClassification::Encrypted + ); + } + + #[test] + fn packer_section_name_detection() { + assert_eq!( + detect_packer_by_section("UPX0"), + Some("UPX") + ); + assert_eq!( + detect_packer_by_section("UPX1"), + Some("UPX") + ); + assert_eq!( + detect_packer_by_section(".vmp0"), + Some("VMProtect") + ); + assert_eq!( + detect_packer_by_section(".themida"), + Some("Themida") + ); + assert_eq!( + detect_packer_by_section(".text"), + None + ); + } + + #[test] + fn section_flags_high_entropy() { + let sections = vec![SectionInfo { + name: ".text".into(), + virtual_address: 0x1000, + virtual_size: 0x1000, + raw_offset: 0, + raw_size: 256, + permissions: SectionPermissions { + read: true, + write: false, + execute: true, + }, + sha256: String::new(), + }]; + + let data: Vec = + (0..=255u8).cycle().take(256).collect(); + let result = + analyze_entropy(&data, §ions, 0x1000); + let text_section = &result.sections[0]; + assert!( + text_section.entropy > HIGH_ENTROPY_THRESHOLD + ); + assert!(text_section + .flags + .contains(&EntropyFlag::HighEntropy)); + assert!(text_section.is_anomalous); + } + + #[test] + fn packer_detection_by_section_name() { + let sections = vec![ + SectionInfo { + name: "UPX0".into(), + virtual_address: 0x1000, + virtual_size: 0x10000, + raw_offset: 0, + raw_size: 0, + permissions: SectionPermissions { + read: true, + write: true, + execute: true, + }, + sha256: String::new(), + }, + SectionInfo { + name: "UPX1".into(), + virtual_address: 0x11000, + virtual_size: 0x5000, + raw_offset: 0x200, + raw_size: 0x4000, + permissions: SectionPermissions { + read: true, + write: false, + execute: true, + }, + sha256: String::new(), + }, + ]; + + let data = vec![0u8; 0x4200]; + let result = + analyze_entropy(&data, §ions, 0x11000); + assert!(result.packing_detected); + assert_eq!( + result.packer_name, + Some("UPX".into()) + ); + assert!(!result.packing_indicators.is_empty()); + } + + #[test] + fn elf_entropy_analysis() { + let data = load_fixture("hello_elf"); + let format_result = + crate::formats::parse_format(&data) + .unwrap(); + let result = analyze_entropy( + &data, + &format_result.sections, + format_result.entry_point, + ); + + assert!(result.overall_entropy > 0.0); + assert!(!result.sections.is_empty()); + assert!(!result.packing_detected); + } + + #[test] + fn entropy_pass_populates_context() { + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + + crate::passes::format::FormatPass + .run(&mut ctx) + .unwrap(); + assert!(ctx.format_result.is_some()); + + EntropyPass.run(&mut ctx).unwrap(); + assert!(ctx.entropy_result.is_some()); + + let result = ctx.entropy_result.unwrap(); + assert!(result.overall_entropy > 0.0); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs new file mode 100644 index 0000000..628d4bd --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs @@ -0,0 +1,183 @@ +// ©AngelaMos | 2026 +// format.rs + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::formats; +use crate::pass::{AnalysisPass, Sealed}; + +pub struct FormatPass; + +impl Sealed for FormatPass {} + +impl AnalysisPass for FormatPass { + fn name(&self) -> &'static str { + "format" + } + + fn dependencies(&self) -> &[&'static str] { + &[] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let result = + formats::parse_format(ctx.data())?; + ctx.format_result = Some(result); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::context::{AnalysisContext, BinarySource}; + use crate::formats::{self, FormatAnomaly}; + use crate::types::{Architecture, BinaryFormat, Endianness}; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + fn make_ctx(data: Vec) -> AnalysisContext { + let size = data.len() as u64; + AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ) + } + + #[test] + fn parse_elf_basic_metadata() { + let data = load_fixture("hello_elf"); + let result = + formats::parse_format(&data).unwrap(); + + assert_eq!(result.format, BinaryFormat::Elf); + assert_eq!( + result.architecture, + Architecture::X86_64 + ); + assert_eq!(result.bits, 64); + assert_eq!( + result.endianness, + Endianness::Little + ); + assert!(result.entry_point > 0); + assert!(result.is_pie); + assert!(!result.is_stripped); + } + + #[test] + fn parse_elf_sections_present() { + let data = load_fixture("hello_elf"); + let result = + formats::parse_format(&data).unwrap(); + + assert!(!result.sections.is_empty()); + let text = result + .sections + .iter() + .find(|s| s.name == ".text"); + assert!(text.is_some()); + assert!(text.unwrap().permissions.execute); + } + + #[test] + fn parse_elf_segments_present() { + let data = load_fixture("hello_elf"); + let result = + formats::parse_format(&data).unwrap(); + + assert!(!result.segments.is_empty()); + let load_segments: Vec<_> = result + .segments + .iter() + .filter(|s| { + s.name.as_deref() == Some("LOAD") + }) + .collect(); + assert!(!load_segments.is_empty()); + } + + #[test] + fn parse_elf_stripped_detection() { + let data = + load_fixture("hello_elf_stripped"); + let result = + formats::parse_format(&data).unwrap(); + + assert!(result.is_stripped); + assert!(result.anomalies.iter().any(|a| { + matches!( + a, + FormatAnomaly::StrippedBinary + ) + })); + } + + #[test] + fn parse_elf_info_populated() { + let data = load_fixture("hello_elf"); + let result = + formats::parse_format(&data).unwrap(); + + let elf_info = result.elf_info.unwrap(); + assert!(!elf_info.os_abi.is_empty()); + assert!(!elf_info.elf_type.is_empty()); + assert!(elf_info.interpreter.is_some()); + assert!(elf_info.gnu_relro); + } + + #[test] + fn parse_elf_section_hashes() { + let data = load_fixture("hello_elf"); + let result = + formats::parse_format(&data).unwrap(); + + let text = result + .sections + .iter() + .find(|s| s.name == ".text") + .unwrap(); + assert!( + !text.sha256.is_empty(), + ".text section should have a hash" + ); + assert_eq!(text.sha256.len(), 64); + } + + #[test] + fn parse_invalid_binary() { + let data = vec![0xDE, 0xAD, 0xBE, 0xEF]; + let result = formats::parse_format(&data); + assert!(result.is_err()); + } + + #[test] + fn format_pass_populates_context() { + use crate::pass::AnalysisPass; + use super::FormatPass; + + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + assert!(ctx.format_result.is_none()); + + FormatPass.run(&mut ctx).unwrap(); + assert!(ctx.format_result.is_some()); + + let fmt = ctx.format_result.unwrap(); + assert_eq!(fmt.format, BinaryFormat::Elf); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs new file mode 100644 index 0000000..1a35d25 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs @@ -0,0 +1,908 @@ +// ©AngelaMos | 2026 +// imports.rs + +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::pass::{AnalysisPass, Sealed}; +use crate::types::Severity; + +pub struct SuspiciousApiDef { + pub name: &'static str, + pub tag: &'static str, + pub mitre_id: &'static str, +} + +pub const SUSPICIOUS_APIS: &[SuspiciousApiDef] = &[ + SuspiciousApiDef { + name: "VirtualAllocEx", + tag: "injection", + mitre_id: "T1055", + }, + SuspiciousApiDef { + name: "WriteProcessMemory", + tag: "injection", + mitre_id: "T1055", + }, + SuspiciousApiDef { + name: "CreateRemoteThread", + tag: "injection", + mitre_id: "T1055", + }, + SuspiciousApiDef { + name: "NtUnmapViewOfSection", + tag: "hollowing", + mitre_id: "T1055.012", + }, + SuspiciousApiDef { + name: "SetThreadContext", + tag: "hollowing", + mitre_id: "T1055.012", + }, + SuspiciousApiDef { + name: "QueueUserAPC", + tag: "apc-injection", + mitre_id: "T1055.004", + }, + SuspiciousApiDef { + name: "IsDebuggerPresent", + tag: "anti-debug", + mitre_id: "T1622", + }, + SuspiciousApiDef { + name: "NtQueryInformationProcess", + tag: "anti-debug", + mitre_id: "T1622", + }, + SuspiciousApiDef { + name: "OpenProcessToken", + tag: "token-manipulation", + mitre_id: "T1134", + }, + SuspiciousApiDef { + name: "AdjustTokenPrivileges", + tag: "token-manipulation", + mitre_id: "T1134", + }, + SuspiciousApiDef { + name: "RegSetValueEx", + tag: "persistence", + mitre_id: "T1547.001", + }, + SuspiciousApiDef { + name: "CreateService", + tag: "persistence", + mitre_id: "T1543.003", + }, + SuspiciousApiDef { + name: "URLDownloadToFile", + tag: "download", + mitre_id: "T1105", + }, + SuspiciousApiDef { + name: "InternetOpen", + tag: "network", + mitre_id: "T1071", + }, + SuspiciousApiDef { + name: "CryptDecrypt", + tag: "deobfuscation", + mitre_id: "T1140", + }, + SuspiciousApiDef { + name: "ptrace", + tag: "injection", + mitre_id: "T1055.008", + }, + SuspiciousApiDef { + name: "mprotect", + tag: "memory-manipulation", + mitre_id: "", + }, + SuspiciousApiDef { + name: "dlopen", + tag: "loading", + mitre_id: "T1574.006", + }, + SuspiciousApiDef { + name: "dlsym", + tag: "loading", + mitre_id: "T1574.006", + }, + SuspiciousApiDef { + name: "execve", + tag: "execution", + mitre_id: "T1059", + }, + SuspiciousApiDef { + name: "process_vm_readv", + tag: "injection", + mitre_id: "T1055", + }, + SuspiciousApiDef { + name: "process_vm_writev", + tag: "injection", + mitre_id: "T1055", + }, +]; + +struct CombinationDef { + name: &'static str, + description: &'static str, + patterns: &'static [&'static str], + mitre_id: &'static str, + severity: Severity, +} + +const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[ + CombinationDef { + name: "Process Injection Chain", + description: "VirtualAllocEx + WriteProcessMemory \ + + CreateRemoteThread", + patterns: &[ + "VirtualAllocEx", + "WriteProcessMemory", + "CreateRemoteThread", + ], + mitre_id: "T1055", + severity: Severity::Critical, + }, + CombinationDef { + name: "Process Hollowing", + description: "CreateProcess + \ + NtUnmapViewOfSection + \ + SetThreadContext + ResumeThread", + patterns: &[ + "CreateProcess*", + "NtUnmapViewOfSection", + "SetThreadContext", + "ResumeThread", + ], + mitre_id: "T1055.012", + severity: Severity::Critical, + }, + CombinationDef { + name: "APC Injection", + description: "QueueUserAPC + OpenThread", + patterns: &["QueueUserAPC", "OpenThread"], + mitre_id: "T1055.004", + severity: Severity::High, + }, + CombinationDef { + name: "DLL Injection", + description: "LoadLibrary + CreateRemoteThread", + patterns: &[ + "LoadLibrary*", + "CreateRemoteThread", + ], + mitre_id: "T1055.001", + severity: Severity::High, + }, + CombinationDef { + name: "Credential Theft", + description: "OpenProcess + ReadProcessMemory", + patterns: &[ + "OpenProcess", + "ReadProcessMemory", + ], + mitre_id: "T1003", + severity: Severity::Critical, + }, + CombinationDef { + name: "Service Persistence", + description: "OpenSCManager + CreateService", + patterns: &[ + "OpenSCManager*", + "CreateService*", + ], + mitre_id: "T1543.003", + severity: Severity::Medium, + }, + CombinationDef { + name: "Registry Persistence", + description: "RegOpenKeyEx + RegSetValueEx", + patterns: &[ + "RegOpenKeyEx*", + "RegSetValueEx*", + ], + mitre_id: "T1547.001", + severity: Severity::Medium, + }, + CombinationDef { + name: "Download and Execute", + description: "URLDownloadToFile + ShellExecute", + patterns: &[ + "URLDownloadToFile*", + "ShellExecute*", + ], + mitre_id: "T1105", + severity: Severity::High, + }, + CombinationDef { + name: "Download and Execute", + description: "URLDownloadToFile + WinExec", + patterns: &["URLDownloadToFile*", "WinExec"], + mitre_id: "T1105", + severity: Severity::High, + }, + CombinationDef { + name: "Linux ptrace Injection", + description: "ptrace-based process injection", + patterns: &["ptrace"], + mitre_id: "T1055.008", + severity: Severity::High, + }, + CombinationDef { + name: "Linux RWX Memory", + description: "mmap + mprotect for RWX memory", + patterns: &["mmap", "mprotect"], + mitre_id: "", + severity: Severity::Medium, + }, + CombinationDef { + name: "Linux C2 Connection", + description: "socket + connect + inet_pton \ + hardcoded C2 address", + patterns: &[ + "socket", + "connect", + "inet_pton", + ], + mitre_id: "T1071", + severity: Severity::High, + }, + CombinationDef { + name: "Linux Network Listener", + description: "socket + bind + listen + accept \ + backdoor listener", + patterns: &[ + "socket", + "bind", + "listen", + "accept", + ], + mitre_id: "T1571", + severity: Severity::High, + }, + CombinationDef { + name: "Linux Dynamic Loading", + description: "dlopen + dlsym runtime \ + API resolution", + patterns: &["dlopen", "dlsym"], + mitre_id: "T1574.006", + severity: Severity::Medium, + }, + CombinationDef { + name: "Linux Process Injection", + description: "process_vm_writev \ + cross-process memory write", + patterns: &["process_vm_writev"], + mitre_id: "T1055", + severity: Severity::Critical, + }, +]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportResult { + pub imports: Vec, + pub exports: Vec, + pub libraries: Vec, + pub suspicious_combinations: + Vec, + pub mitre_mappings: Vec, + pub statistics: ImportStatistics, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportEntry { + pub library: String, + pub function: String, + pub address: Option, + pub ordinal: Option, + pub is_suspicious: bool, + pub threat_tags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportEntry { + pub name: Option, + pub address: u64, + pub ordinal: Option, + pub is_forwarded: bool, + pub forward_target: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SuspiciousCombination { + pub name: String, + pub description: String, + pub apis: Vec, + pub mitre_id: String, + pub severity: Severity, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MitreMapping { + pub technique_id: String, + pub api: String, + pub tag: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportStatistics { + pub total_imports: usize, + pub total_exports: usize, + pub suspicious_count: usize, + pub library_count: usize, +} + +pub struct ImportPass; + +impl Sealed for ImportPass {} + +impl AnalysisPass for ImportPass { + fn name(&self) -> &'static str { + "imports" + } + + fn dependencies(&self) -> &[&'static str] { + &["format"] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let result = analyze_imports(ctx.data())?; + ctx.import_result = Some(result); + Ok(()) + } +} + +fn analyze_imports( + data: &[u8], +) -> Result { + let object = + goblin::Object::parse(data).map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + + let (imports, exports, libraries) = match &object { + goblin::Object::Elf(elf) => extract_elf(elf), + goblin::Object::PE(pe) => extract_pe(pe), + goblin::Object::Mach(mach) => { + extract_mach(mach, data)? + } + _ => (Vec::new(), Vec::new(), Vec::new()), + }; + + let suspicious_combinations = + detect_combinations(&imports); + let mitre_mappings = + collect_mitre_mappings(&imports); + let suspicious_count = imports + .iter() + .filter(|i| i.is_suspicious) + .count(); + + let statistics = ImportStatistics { + total_imports: imports.len(), + total_exports: exports.len(), + suspicious_count, + library_count: libraries.len(), + }; + + Ok(ImportResult { + imports, + exports, + libraries, + suspicious_combinations, + mitre_mappings, + statistics, + }) +} + +fn extract_elf( + elf: &goblin::elf::Elf, +) -> (Vec, Vec, Vec) { + let libraries: Vec = elf + .libraries + .iter() + .map(|s| s.to_string()) + .collect(); + + let mut imports = Vec::new(); + for sym in elf.dynsyms.iter() { + if !sym.is_import() || sym.st_name == 0 { + continue; + } + let name = elf + .dynstrtab + .get_at(sym.st_name) + .unwrap_or(""); + if name.is_empty() { + continue; + } + let (is_suspicious, threat_tags) = + flag_suspicious(name); + imports.push(ImportEntry { + library: String::new(), + function: name.to_string(), + address: None, + ordinal: None, + is_suspicious, + threat_tags, + }); + } + + let mut exports = Vec::new(); + for sym in elf.dynsyms.iter() { + if sym.is_import() + || sym.st_value == 0 + || sym.st_name == 0 + { + continue; + } + let name = elf + .dynstrtab + .get_at(sym.st_name) + .unwrap_or(""); + if name.is_empty() { + continue; + } + exports.push(ExportEntry { + name: Some(name.to_string()), + address: sym.st_value, + ordinal: None, + is_forwarded: false, + forward_target: None, + }); + } + + (imports, exports, libraries) +} + +fn extract_pe( + pe: &goblin::pe::PE, +) -> (Vec, Vec, Vec) { + let mut lib_set = HashSet::new(); + let mut imports = Vec::new(); + + for import in &pe.imports { + let dll = import.dll.to_string(); + lib_set.insert(dll.clone()); + let (is_suspicious, threat_tags) = + flag_suspicious(&import.name); + imports.push(ImportEntry { + library: dll, + function: import.name.to_string(), + address: Some(import.rva as u64), + ordinal: Some(import.ordinal), + is_suspicious, + threat_tags, + }); + } + + let mut exports = Vec::new(); + for export in &pe.exports { + let is_forwarded = export.reexport.is_some(); + let forward_target = + export.reexport.as_ref().map(|r| match r { + goblin::pe::export::Reexport::DLLName { + export: name, + lib, + } => format!("{lib}!{name}"), + goblin::pe::export::Reexport::DLLOrdinal { + ordinal, + lib, + } => format!("{lib}!#{ordinal}"), + }); + exports.push(ExportEntry { + name: export.name.map(|s| s.to_string()), + address: export.rva as u64, + ordinal: None, + is_forwarded, + forward_target, + }); + } + + let libraries: Vec = + lib_set.into_iter().collect(); + (imports, exports, libraries) +} + +fn extract_mach( + mach: &goblin::mach::Mach, + data: &[u8], +) -> Result< + (Vec, Vec, Vec), + EngineError, +> { + match mach { + goblin::mach::Mach::Binary(macho) => { + Ok(extract_single_macho(macho)) + } + goblin::mach::Mach::Fat(fat) => { + for arch in fat.iter_arches() { + let arch = arch.map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + let macho = goblin::mach::MachO::parse( + data, + arch.offset as usize, + ) + .map_err(|e| { + EngineError::InvalidBinary { + reason: e.to_string(), + } + })?; + return Ok(extract_single_macho(&macho)); + } + Ok((Vec::new(), Vec::new(), Vec::new())) + } + } +} + +fn extract_single_macho( + macho: &goblin::mach::MachO, +) -> (Vec, Vec, Vec) { + let mut imports = Vec::new(); + if let Ok(macho_imports) = macho.imports() { + for imp in &macho_imports { + let (is_suspicious, threat_tags) = + flag_suspicious(imp.name); + imports.push(ImportEntry { + library: imp.dylib.to_string(), + function: imp.name.to_string(), + address: Some(imp.address), + ordinal: None, + is_suspicious, + threat_tags, + }); + } + } + + let mut exports = Vec::new(); + if let Ok(macho_exports) = macho.exports() { + for exp in &macho_exports { + exports.push(ExportEntry { + name: Some(exp.name.clone()), + address: exp.offset, + ordinal: None, + is_forwarded: false, + forward_target: None, + }); + } + } + + let libraries: Vec = macho + .libs + .iter() + .filter(|lib| !lib.is_empty()) + .map(|lib| lib.to_string()) + .collect(); + + (imports, exports, libraries) +} + +fn flag_suspicious( + name: &str, +) -> (bool, Vec) { + let mut tags = Vec::new(); + for api in SUSPICIOUS_APIS { + if matches_api(name, api.name) { + tags.push(api.tag.to_string()); + } + } + let is_suspicious = !tags.is_empty(); + (is_suspicious, tags) +} + +fn matches_api( + import_name: &str, + api_name: &str, +) -> bool { + if import_name == api_name { + return true; + } + if import_name.starts_with(api_name) { + let suffix = &import_name[api_name.len()..]; + return suffix == "A" || suffix == "W"; + } + false +} + +fn matches_pattern( + import_name: &str, + pattern: &str, +) -> bool { + if let Some(prefix) = pattern.strip_suffix('*') { + import_name.starts_with(prefix) + } else { + matches_api(import_name, pattern) + } +} + +fn detect_combinations( + imports: &[ImportEntry], +) -> Vec { + let function_names: Vec<&str> = imports + .iter() + .map(|i| i.function.as_str()) + .collect(); + let mut results = Vec::new(); + let mut seen = HashSet::new(); + + for combo in SUSPICIOUS_COMBINATIONS { + if seen.contains(combo.name) { + continue; + } + let all_matched = + combo.patterns.iter().all(|pattern| { + function_names.iter().any(|name| { + matches_pattern(name, pattern) + }) + }); + if !all_matched { + continue; + } + + let matched_apis: Vec = combo + .patterns + .iter() + .filter_map(|pattern| { + function_names + .iter() + .find(|name| { + matches_pattern(name, pattern) + }) + .map(|name| name.to_string()) + }) + .collect(); + + results.push(SuspiciousCombination { + name: combo.name.into(), + description: combo.description.into(), + apis: matched_apis, + mitre_id: combo.mitre_id.into(), + severity: combo.severity.clone(), + }); + seen.insert(combo.name); + } + + results +} + +fn collect_mitre_mappings( + imports: &[ImportEntry], +) -> Vec { + let mut mappings = Vec::new(); + for import in imports { + if !import.is_suspicious { + continue; + } + for api in SUSPICIOUS_APIS { + if matches_api(&import.function, api.name) + && !api.mitre_id.is_empty() + { + mappings.push(MitreMapping { + technique_id: api.mitre_id.into(), + api: import.function.clone(), + tag: api.tag.into(), + }); + } + } + } + mappings +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::context::BinarySource; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + fn make_ctx(data: Vec) -> AnalysisContext { + let size = data.len() as u64; + AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ) + } + + #[test] + fn elf_imports_extracted() { + let data = load_fixture("hello_elf"); + let result = + analyze_imports(&data).unwrap(); + + assert!( + !result.imports.is_empty(), + "ELF binary should have imports" + ); + assert!( + !result.libraries.is_empty(), + "ELF binary should list needed libraries" + ); + assert!(result.statistics.total_imports > 0); + } + + #[test] + fn suspicious_api_flagging() { + let (is_suspicious, tags) = + flag_suspicious("VirtualAllocEx"); + assert!(is_suspicious); + assert!(tags.contains(&"injection".to_string())); + + let (is_suspicious, tags) = + flag_suspicious("RegSetValueExW"); + assert!(is_suspicious); + assert!(tags.contains( + &"persistence".to_string() + )); + + let (is_suspicious, _) = + flag_suspicious("printf"); + assert!(!is_suspicious); + } + + #[test] + fn combination_detection_injection_chain() { + let imports = vec![ + ImportEntry { + library: "kernel32.dll".into(), + function: "VirtualAllocEx".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec![ + "injection".into(), + ], + }, + ImportEntry { + library: "kernel32.dll".into(), + function: "WriteProcessMemory".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec![ + "injection".into(), + ], + }, + ImportEntry { + library: "kernel32.dll".into(), + function: "CreateRemoteThread".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec![ + "injection".into(), + ], + }, + ]; + + let combos = detect_combinations(&imports); + assert_eq!(combos.len(), 1); + assert_eq!( + combos[0].name, + "Process Injection Chain" + ); + assert_eq!(combos[0].mitre_id, "T1055"); + assert_eq!( + combos[0].severity, + Severity::Critical + ); + } + + #[test] + fn combination_with_aw_suffix() { + let imports = vec![ + ImportEntry { + library: "advapi32.dll".into(), + function: "RegOpenKeyExA".into(), + address: None, + ordinal: None, + is_suspicious: false, + threat_tags: vec![], + }, + ImportEntry { + library: "advapi32.dll".into(), + function: "RegSetValueExW".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec![ + "persistence".into(), + ], + }, + ]; + + let combos = detect_combinations(&imports); + assert!(combos + .iter() + .any(|c| c.name + == "Registry Persistence")); + } + + #[test] + fn no_false_positive_combinations() { + let imports = vec![ImportEntry { + library: "kernel32.dll".into(), + function: "VirtualAllocEx".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec!["injection".into()], + }]; + + let combos = detect_combinations(&imports); + assert!( + !combos.iter().any(|c| c.name + == "Process Injection Chain"), + "should not detect chain with only one API" + ); + } + + #[test] + fn mitre_mappings_collected() { + let imports = vec![ + ImportEntry { + library: String::new(), + function: "ptrace".into(), + address: None, + ordinal: None, + is_suspicious: true, + threat_tags: vec![ + "injection".into(), + ], + }, + ImportEntry { + library: String::new(), + function: "printf".into(), + address: None, + ordinal: None, + is_suspicious: false, + threat_tags: vec![], + }, + ]; + + let mappings = + collect_mitre_mappings(&imports); + assert_eq!(mappings.len(), 1); + assert_eq!( + mappings[0].technique_id, + "T1055.008" + ); + assert_eq!(mappings[0].api, "ptrace"); + } + + #[test] + fn import_pass_populates_context() { + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + assert!(ctx.import_result.is_none()); + + ImportPass.run(&mut ctx).unwrap(); + assert!(ctx.import_result.is_some()); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs new file mode 100644 index 0000000..cee2788 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs @@ -0,0 +1,9 @@ +// ©AngelaMos | 2026 +// mod.rs + +pub mod disasm; +pub mod entropy; +pub mod format; +pub mod imports; +pub mod strings; +pub mod threat; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs new file mode 100644 index 0000000..71479d5 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs @@ -0,0 +1,897 @@ +// ©AngelaMos | 2026 +// strings.rs + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::formats::SectionInfo; +use crate::pass::{AnalysisPass, Sealed}; +use crate::passes::imports::SUSPICIOUS_APIS; +use crate::types::{StringCategory, StringEncoding}; + +const MIN_STRING_LENGTH: usize = 4; + +const PRINTABLE_MIN: u8 = 0x20; +const PRINTABLE_MAX: u8 = 0x7E; +const TAB: u8 = 0x09; +const NEWLINE: u8 = 0x0A; +const CARRIAGE_RETURN: u8 = 0x0D; + +const SUSPICIOUS_CATEGORIES: &[StringCategory] = &[ + StringCategory::SuspiciousApi, + StringCategory::PackerSignature, + StringCategory::AntiAnalysis, + StringCategory::PersistencePath, + StringCategory::EncodedData, + StringCategory::ShellCommand, + StringCategory::CryptoWallet, +]; + +const URL_PREFIXES: &[&str] = + &["http://", "https://", "ftp://"]; + +const UNIX_PATH_PREFIXES: &[&str] = &[ + "/etc/", "/tmp/", "/var/", "/bin/", "/usr/", + "/dev/", "/proc/", "/sys/", "/opt/", "/home/", + "/root/", "/lib/", "/sbin/", +]; + +const REGISTRY_PREFIXES: &[&str] = &[ + "HKEY_", + "HKLM\\", + "HKCU\\", + "HKCR\\", + "HKCC\\", + "HKU\\", +]; + +const SHELL_INDICATORS: &[&str] = &[ + "cmd.exe", + "cmd /c", + "cmd /k", + "powershell", + "pwsh", + "/bin/sh", + "/bin/bash", + "/bin/zsh", + "bash -c", + "sh -c", + "| bash", + "|bash", + "| /bin/sh", + "|/bin/sh", + "| /bin/bash", + "|/bin/bash", +]; + +const PACKER_SIGNATURES: &[&str] = &[ + "UPX!", "MPRESS", ".themida", ".vmp", ".enigma", + "PEC2", "ASPack", "MEW ", +]; + +const DEBUG_ARTIFACTS: &[&str] = &[ + "/rustc/", + ".cargo/registry/", + "panicked at", + ".pdb", + "_ZN", + ".debug_", + "DWARF", +]; + +const ANTI_ANALYSIS_INDICATORS: &[&str] = &[ + "VMware", + "VirtualBox", + "VBox", + "QEMU", + "sandbox", + "Sandboxie", + "wireshark", + "procmon", + "x64dbg", + "x32dbg", + "ollydbg", + "IDA Pro", + "Ghidra", + "Immunity", + "SbieDll", + "dbghelp", + "wine_get_unix_file_name", + "TracerPid", + "/proc/self/status", + "/proc/self/maps", +]; + +const PERSISTENCE_PATHS: &[&str] = &[ + "CurrentVersion\\Run", + "CurrentVersion\\RunOnce", + "CurrentVersion\\RunServices", + "/etc/cron", + "/etc/init.d/", + "/etc/systemd/", + ".bashrc", + ".bash_profile", + ".profile", + "/etc/rc.local", + "crontab", + "launchd", + "LaunchAgents", + "LaunchDaemons", + ".config/autostart", +]; + +const BASE64_CHARS: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +const BASE64_MIN_LENGTH: usize = 20; + +const IP_OCTET_MAX: u32 = 255; +const IP_OCTET_COUNT: usize = 4; + +const BTC_MIN_LENGTH: usize = 26; +const BTC_MAX_LENGTH: usize = 35; +const ETH_ADDRESS_LENGTH: usize = 42; +const ETH_PREFIX: &str = "0x"; +const ETH_HEX_DIGITS: usize = 40; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringResult { + pub strings: Vec, + pub statistics: StringStatistics, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtractedString { + pub value: String, + pub offset: u64, + pub encoding: StringEncoding, + pub length: usize, + pub category: StringCategory, + pub is_suspicious: bool, + pub section: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringStatistics { + pub total: usize, + pub by_encoding: HashMap, + pub by_category: HashMap, + pub suspicious_count: usize, +} + +pub struct StringPass; + +impl Sealed for StringPass {} + +impl AnalysisPass for StringPass { + fn name(&self) -> &'static str { + "strings" + } + + fn dependencies(&self) -> &[&'static str] { + &["format"] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let sections = ctx + .format_result + .as_ref() + .map(|fr| fr.sections.as_slice()); + let result = + extract_strings(ctx.data(), sections); + ctx.string_result = Some(result); + Ok(()) + } +} + +fn extract_strings( + data: &[u8], + sections: Option<&[SectionInfo]>, +) -> StringResult { + let mut strings = Vec::new(); + + extract_ascii(data, sections, &mut strings); + extract_utf16le(data, sections, &mut strings); + + let mut by_encoding = HashMap::new(); + let mut by_category = HashMap::new(); + let mut suspicious_count = 0; + + for s in &strings { + *by_encoding + .entry(s.encoding.clone()) + .or_insert(0) += 1; + *by_category + .entry(s.category.clone()) + .or_insert(0) += 1; + if s.is_suspicious { + suspicious_count += 1; + } + } + + let total = strings.len(); + let statistics = StringStatistics { + total, + by_encoding, + by_category, + suspicious_count, + }; + + StringResult { + strings, + statistics, + } +} + +fn is_printable(b: u8) -> bool { + (PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b) + || b == TAB + || b == NEWLINE + || b == CARRIAGE_RETURN +} + +fn extract_ascii( + data: &[u8], + sections: Option<&[SectionInfo]>, + out: &mut Vec, +) { + let mut start = 0; + let mut in_run = false; + + for (i, &byte) in data.iter().enumerate() { + if is_printable(byte) { + if !in_run { + start = i; + in_run = true; + } + } else if in_run { + let len = i - start; + if len >= MIN_STRING_LENGTH { + if let Ok(s) = + std::str::from_utf8(&data[start..i]) + { + let is_multibyte = s + .bytes() + .any(|b| !b.is_ascii()); + let encoding = if is_multibyte { + StringEncoding::Utf8 + } else { + StringEncoding::Ascii + }; + let category = classify(s); + let is_suspicious = + is_category_suspicious(&category); + let section = sections.and_then(|secs| { + find_section( + secs, + start as u64, + ) + }); + out.push(ExtractedString { + value: s.to_string(), + offset: start as u64, + encoding, + length: len, + category, + is_suspicious, + section, + }); + } + } + in_run = false; + } + } + + if in_run { + let len = data.len() - start; + if len >= MIN_STRING_LENGTH { + if let Ok(s) = + std::str::from_utf8(&data[start..]) + { + let is_multibyte = + s.bytes().any(|b| !b.is_ascii()); + let encoding = if is_multibyte { + StringEncoding::Utf8 + } else { + StringEncoding::Ascii + }; + let category = classify(s); + let is_suspicious = + is_category_suspicious(&category); + let section = sections.and_then(|secs| { + find_section(secs, start as u64) + }); + out.push(ExtractedString { + value: s.to_string(), + offset: start as u64, + encoding, + length: len, + category, + is_suspicious, + section, + }); + } + } + } +} + +fn extract_utf16le( + data: &[u8], + sections: Option<&[SectionInfo]>, + out: &mut Vec, +) { + let mut i = 0; + while i + 1 < data.len() { + let lo = data[i]; + let hi = data[i + 1]; + + if hi == 0x00 && is_printable(lo) { + let start = i; + let mut code_units = Vec::new(); + let mut pos = i; + + while pos + 1 < data.len() { + let clo = data[pos]; + let chi = data[pos + 1]; + if chi == 0x00 && clo == 0x00 { + break; + } + if chi == 0x00 && is_printable(clo) { + code_units.push(u16::from(clo)); + pos += 2; + } else { + break; + } + } + + if code_units.len() >= MIN_STRING_LENGTH { + let value = + String::from_utf16_lossy(&code_units); + let category = classify(&value); + let is_suspicious = + is_category_suspicious(&category); + let section = sections.and_then(|secs| { + find_section(secs, start as u64) + }); + out.push(ExtractedString { + value, + offset: start as u64, + encoding: StringEncoding::Utf16Le, + length: code_units.len(), + category, + is_suspicious, + section, + }); + } + + i = if pos > i { pos } else { i + 2 }; + } else { + i += 2; + } + } +} + +fn find_section( + sections: &[SectionInfo], + file_offset: u64, +) -> Option { + sections + .iter() + .find(|s| { + s.raw_size > 0 + && file_offset >= s.raw_offset + && file_offset + < s.raw_offset + s.raw_size + }) + .map(|s| s.name.clone()) +} + +fn classify(s: &str) -> StringCategory { + if is_url(s) { + return StringCategory::Url; + } + if is_ip_address(s) { + return StringCategory::IpAddress; + } + if is_registry_key(s) { + return StringCategory::RegistryKey; + } + if is_shell_command(s) { + return StringCategory::ShellCommand; + } + if is_persistence_path(s) { + return StringCategory::PersistencePath; + } + if is_anti_analysis(s) { + return StringCategory::AntiAnalysis; + } + if is_packer_signature(s) { + return StringCategory::PackerSignature; + } + if is_suspicious_api(s) { + return StringCategory::SuspiciousApi; + } + if is_debug_artifact(s) { + return StringCategory::DebugArtifact; + } + if is_file_path(s) { + return StringCategory::FilePath; + } + if is_crypto_wallet(s) { + return StringCategory::CryptoWallet; + } + if is_email(s) { + return StringCategory::Email; + } + if is_encoded_data(s) { + return StringCategory::EncodedData; + } + StringCategory::Generic +} + +fn is_category_suspicious( + category: &StringCategory, +) -> bool { + SUSPICIOUS_CATEGORIES.contains(category) +} + +fn is_url(s: &str) -> bool { + let lower = s.to_ascii_lowercase(); + URL_PREFIXES + .iter() + .any(|prefix| lower.starts_with(prefix)) +} + +fn is_ip_address(s: &str) -> bool { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != IP_OCTET_COUNT { + return false; + } + parts.iter().all(|part| { + if part.is_empty() || part.len() > 3 { + return false; + } + match part.parse::() { + Ok(val) => val <= IP_OCTET_MAX, + Err(_) => false, + } + }) +} + +fn is_file_path(s: &str) -> bool { + if s.len() >= 3 + && s.as_bytes()[0].is_ascii_uppercase() + && s.as_bytes()[1] == b':' + && s.as_bytes()[2] == b'\\' + { + return true; + } + if s.starts_with("\\\\") { + return true; + } + UNIX_PATH_PREFIXES + .iter() + .any(|prefix| s.starts_with(prefix)) +} + +fn is_registry_key(s: &str) -> bool { + REGISTRY_PREFIXES + .iter() + .any(|prefix| s.starts_with(prefix)) +} + +fn is_shell_command(s: &str) -> bool { + let lower = s.to_ascii_lowercase(); + SHELL_INDICATORS + .iter() + .any(|indicator| lower.contains(indicator)) +} + +fn is_crypto_wallet(s: &str) -> bool { + if s.len() >= BTC_MIN_LENGTH + && s.len() <= BTC_MAX_LENGTH + && (s.starts_with('1') || s.starts_with('3')) + && s.chars() + .all(|c| c.is_ascii_alphanumeric()) + && !s.chars().any(|c| { + c == '0' || c == 'O' || c == 'I' || c == 'l' + }) + { + return true; + } + + if s.len() == ETH_ADDRESS_LENGTH + && s.starts_with(ETH_PREFIX) + && s[2..].len() == ETH_HEX_DIGITS + && s[2..].chars().all(|c| c.is_ascii_hexdigit()) + { + return true; + } + + false +} + +fn is_email(s: &str) -> bool { + let at_pos = match s.find('@') { + Some(p) if p > 0 => p, + _ => return false, + }; + let domain = &s[at_pos + 1..]; + let dot_pos = match domain.rfind('.') { + Some(p) if p > 0 => p, + _ => return false, + }; + let tld = &domain[dot_pos + 1..]; + if tld.len() < 2 { + return false; + } + let local = &s[..at_pos]; + local + .chars() + .all(|c| c.is_ascii_alphanumeric() + || c == '.' + || c == '_' + || c == '%' + || c == '+' + || c == '-') + && domain[..dot_pos] + .chars() + .all(|c| c.is_ascii_alphanumeric() + || c == '.' + || c == '-') + && tld.chars().all(|c| c.is_ascii_alphabetic()) +} + +fn is_suspicious_api(s: &str) -> bool { + SUSPICIOUS_APIS + .iter() + .any(|api| s == api.name) +} + +fn is_packer_signature(s: &str) -> bool { + PACKER_SIGNATURES + .iter() + .any(|sig| s.contains(sig)) +} + +fn is_debug_artifact(s: &str) -> bool { + DEBUG_ARTIFACTS + .iter() + .any(|artifact| s.contains(artifact)) +} + +fn is_anti_analysis(s: &str) -> bool { + let lower = s.to_ascii_lowercase(); + ANTI_ANALYSIS_INDICATORS.iter().any(|indicator| { + lower.contains(&indicator.to_ascii_lowercase()) + }) +} + +fn is_persistence_path(s: &str) -> bool { + PERSISTENCE_PATHS + .iter() + .any(|path| s.contains(path)) +} + +fn is_encoded_data(s: &str) -> bool { + if s.len() < BASE64_MIN_LENGTH { + return false; + } + let trimmed = s.trim_end_matches('='); + if trimmed.is_empty() { + return false; + } + let all_base64 = trimmed + .bytes() + .all(|b| BASE64_CHARS.contains(&b)); + if !all_base64 { + return false; + } + let padding = s.len() - trimmed.len(); + padding <= 2 +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::context::BinarySource; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + fn make_ctx(data: Vec) -> AnalysisContext { + let size = data.len() as u64; + AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ) + } + + #[test] + fn ascii_extraction_min_length() { + let data = + b"abc\x00abcdef\x00ab\x00longstring\x00"; + let result = extract_strings(data, None); + + let values: Vec<&str> = result + .strings + .iter() + .map(|s| s.value.as_str()) + .collect(); + assert!( + !values.contains(&"abc"), + "3-char string should be excluded" + ); + assert!( + !values.contains(&"ab"), + "2-char string should be excluded" + ); + assert!( + values.contains(&"abcdef"), + "6-char string should be included" + ); + assert!( + values.contains(&"longstring"), + "10-char string should be included" + ); + } + + #[test] + fn utf16le_extraction() { + let s = "Hello World!"; + let mut data: Vec = Vec::new(); + for c in s.encode_utf16() { + data.extend_from_slice(&c.to_le_bytes()); + } + data.push(0x00); + data.push(0x00); + + let result = extract_strings(&data, None); + let utf16_strings: Vec<&ExtractedString> = + result + .strings + .iter() + .filter(|s| { + s.encoding == StringEncoding::Utf16Le + }) + .collect(); + assert!( + !utf16_strings.is_empty(), + "should extract UTF-16LE string" + ); + assert!(utf16_strings + .iter() + .any(|s| s.value.contains("Hello"))); + } + + #[test] + fn category_url() { + assert_eq!( + classify("https://evil.com/payload"), + StringCategory::Url, + ); + assert_eq!( + classify("http://malware.ru/dropper"), + StringCategory::Url, + ); + assert_eq!( + classify("ftp://files.example.com"), + StringCategory::Url, + ); + } + + #[test] + fn category_ip_address() { + assert_eq!( + classify("192.168.1.1"), + StringCategory::IpAddress, + ); + assert_eq!( + classify("10.0.0.1"), + StringCategory::IpAddress, + ); + assert_ne!( + classify("999.999.999.999"), + StringCategory::IpAddress, + ); + assert_ne!( + classify("1.2.3"), + StringCategory::IpAddress, + ); + } + + #[test] + fn category_registry_key() { + assert_eq!( + classify("HKLM\\Software\\Microsoft"), + StringCategory::RegistryKey, + ); + assert_eq!( + classify("HKEY_LOCAL_MACHINE"), + StringCategory::RegistryKey, + ); + } + + #[test] + fn category_file_path() { + assert_eq!( + classify("C:\\Windows\\System32\\notepad.exe"), + StringCategory::FilePath, + ); + assert_eq!( + classify("/tmp/output.log"), + StringCategory::FilePath, + ); + assert_eq!( + classify("\\\\server\\share"), + StringCategory::FilePath, + ); + } + + #[test] + fn category_shell_command() { + assert_eq!( + classify("cmd.exe /c whoami"), + StringCategory::ShellCommand, + ); + assert_eq!( + classify("/bin/bash -c echo hi"), + StringCategory::ShellCommand, + ); + } + + #[test] + fn category_packer_signature() { + assert_eq!( + classify("UPX!"), + StringCategory::PackerSignature, + ); + assert_eq!( + classify("This is .themida packed"), + StringCategory::PackerSignature, + ); + } + + #[test] + fn category_anti_analysis() { + assert_eq!( + classify("VMware Virtual Platform"), + StringCategory::AntiAnalysis, + ); + assert_eq!( + classify("wireshark.exe"), + StringCategory::AntiAnalysis, + ); + } + + #[test] + fn category_persistence() { + assert_eq!( + classify( + "Software\\Microsoft\\Windows\\\ + CurrentVersion\\Run" + ), + StringCategory::PersistencePath, + ); + assert_eq!( + classify("/etc/crontab"), + StringCategory::PersistencePath, + ); + } + + #[test] + fn category_encoded_data() { + assert_eq!( + classify( + "SGVsbG8gV29ybGQhIFRoaXMgaXM=" + ), + StringCategory::EncodedData, + ); + assert_ne!( + classify("short"), + StringCategory::EncodedData, + ); + } + + #[test] + fn category_email() { + assert_eq!( + classify("user@example.com"), + StringCategory::Email, + ); + } + + #[test] + fn suspicious_flag_by_category() { + assert!(is_category_suspicious( + &StringCategory::ShellCommand + )); + assert!(is_category_suspicious( + &StringCategory::AntiAnalysis + )); + assert!(is_category_suspicious( + &StringCategory::PackerSignature + )); + assert!(is_category_suspicious( + &StringCategory::PersistencePath + )); + assert!(is_category_suspicious( + &StringCategory::EncodedData + )); + assert!(is_category_suspicious( + &StringCategory::CryptoWallet + )); + assert!(is_category_suspicious( + &StringCategory::SuspiciousApi + )); + assert!(!is_category_suspicious( + &StringCategory::Generic + )); + assert!(!is_category_suspicious( + &StringCategory::Url + )); + } + + #[test] + fn elf_strings_extracted() { + let data = load_fixture("hello_elf"); + let result = extract_strings(&data, None); + + assert!( + !result.strings.is_empty(), + "ELF binary should contain strings" + ); + assert!(result.statistics.total > 0); + } + + #[test] + fn string_pass_populates_context() { + let data = load_fixture("hello_elf"); + let mut ctx = make_ctx(data); + assert!(ctx.string_result.is_none()); + + StringPass.run(&mut ctx).unwrap(); + assert!(ctx.string_result.is_some()); + } + + #[test] + fn section_attribution() { + let sections = vec![SectionInfo { + name: ".rodata".into(), + virtual_address: 0, + virtual_size: 100, + raw_offset: 10, + raw_size: 100, + permissions: + crate::types::SectionPermissions { + read: true, + write: false, + execute: false, + }, + sha256: String::new(), + }]; + + let found = find_section(§ions, 50); + assert_eq!(found, Some(".rodata".into())); + + let not_found = find_section(§ions, 200); + assert_eq!(not_found, None); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs new file mode 100644 index 0000000..531c7c9 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs @@ -0,0 +1,1388 @@ +// ©AngelaMos | 2026 +// threat.rs + +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; + +use crate::context::AnalysisContext; +use crate::error::EngineError; +use crate::formats::{FormatAnomaly, FormatResult}; +use crate::pass::{AnalysisPass, Sealed}; +use crate::passes::entropy::EntropyResult; +use crate::passes::imports::ImportResult; +use crate::passes::strings::StringResult; +use crate::types::{ + EntropyFlag, RiskLevel, StringCategory, +}; +use crate::yara::{YaraMatch, YaraScanner}; + +const IMPORT_MAX: u32 = 20; +const ENTROPY_MAX: u32 = 15; +const PACKING_MAX: u32 = 15; +const STRING_MAX: u32 = 10; +const SECTION_MAX: u32 = 10; +const ENTRY_POINT_MAX: u32 = 10; +const ANTI_ANALYSIS_MAX: u32 = 10; +const YARA_MAX: u32 = 10; + +const INJECTION_CHAIN: u32 = 15; +const HOLLOWING_CHAIN: u32 = 15; +const CREDENTIAL_ACCESS: u32 = 12; +const ANTI_DEBUG_API: u32 = 8; +const REGISTRY_RUN_KEYS: u32 = 7; +const NETWORK_DOWNLOAD: u32 = 6; +const VERY_FEW_IMPORTS: u32 = 5; +const APC_INJECTION: u32 = 8; +const LINUX_PTRACE: u32 = 8; +const LINUX_RWX_MEMORY: u32 = 5; +const LINUX_C2_CONNECTION: u32 = 10; +const LINUX_NETWORK_LISTENER: u32 = 8; +const LINUX_DYNAMIC_LOADING: u32 = 5; +const LINUX_PROCESS_INJECTION: u32 = 15; + +const HIGH_SECTION_ENTROPY: u32 = 6; +const HIGH_SECTION_ENTROPY_CAP: u32 = 2; +const VERY_HIGH_OVERALL_ENTROPY: u32 = 3; +const HIGH_ENTROPY_THRESHOLD: f64 = 7.0; +const VERY_HIGH_ENTROPY_THRESHOLD: f64 = 6.8; + +const PACKER_SECTION_NAME: u32 = 5; +const PACKER_SIGNATURE_MATCH: u32 = 3; +const EMPTY_RAW_WITH_VIRTUAL: u32 = 4; +const HIGH_VR_RATIO: u32 = 3; +const PUSHAD_EP: u32 = 3; +const MODIFIED_UPX: u32 = 5; + +const C2_PATTERN: u32 = 5; +const SUSPICIOUS_COMMANDS: u32 = 3; +const BASE64_PE_HEADER: u32 = 4; +const REGISTRY_PERSISTENCE: u32 = 3; +const CRYPTO_WALLET: u32 = 3; + +const RWX_SECTION: u32 = 5; +const EMPTY_SECTION_NAME: u32 = 3; +const UNUSUAL_SECTION_COUNT: u32 = 2; +const ZERO_SIZE_CODE: u32 = 4; + +const EP_OUTSIDE_TEXT: u32 = 5; +const EP_LAST_SECTION: u32 = 5; +const EP_OUTSIDE_ALL: u32 = 7; +const TLS_CALLBACKS: u32 = 3; + +const IS_DEBUGGER_PRESENT: u32 = 3; +const NT_QUERY_INFO_PROCESS: u32 = 5; +const VM_DETECTION_STRINGS: u32 = 3; +const TIMING_CHECK_APIS: u32 = 3; +const SANDBOX_EVASION: u32 = 3; +const LINUX_PTRACE_CHECK: u32 = 5; +const PROC_SELF_ANALYSIS: u32 = 3; + +const YARA_MALWARE_FAMILY: u32 = 10; +const YARA_PACKER_RULE: u32 = 3; +const YARA_SUSPICIOUS: u32 = 5; + +const FEW_IMPORTS_THRESHOLD: usize = 3; +const MAX_NORMAL_SECTIONS: usize = 15; +const BENIGN_MAX: u32 = 15; +const LOW_MAX: u32 = 35; +const MEDIUM_MAX: u32 = 55; +const HIGH_MAX: u32 = 75; +const SUMMARY_TOP_N: usize = 5; + +const SUSPICIOUS_TLDS: &[&str] = &[ + ".ru", ".cn", ".tk", ".pw", ".cc", ".top", + ".xyz", ".buzz", ".onion", +]; + +const BASE64_MZ_PREFIXES: &[&str] = + &["TVqQ", "TVpQ", "TVoA", "TVpB"]; + +const TIMING_CHECK_FUNCTIONS: &[&str] = &[ + "GetTickCount64", + "GetTickCount", + "QueryPerformanceCounter", + "rdtsc", +]; + +const VM_STRINGS: &[&str] = &[ + "vmware", "virtualbox", "vbox", "qemu", + "hyper-v", "xen", +]; + +const SANDBOX_STRINGS: &[&str] = &[ + "sandbox", "cuckoo", "wireshark", "procmon", + "sandboxie", +]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreatResult { + pub total_score: u32, + pub risk_level: RiskLevel, + pub categories: Vec, + pub mitre_techniques: Vec, + pub yara_matches: Vec, + pub summary: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScoringCategory { + pub name: String, + pub score: u32, + pub max_score: u32, + pub details: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScoringDetail { + pub rule: String, + pub points: u32, + pub evidence: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MitreMapping { + pub technique_id: String, + pub technique_name: String, + pub tactic: String, + pub evidence: String, +} + +pub struct ThreatPass; + +impl Sealed for ThreatPass {} + +impl AnalysisPass for ThreatPass { + fn name(&self) -> &'static str { + "threat" + } + + fn dependencies(&self) -> &[&'static str] { + &[ + "format", "imports", "strings", + "entropy", "disasm", + ] + } + + fn run( + &self, + ctx: &mut AnalysisContext, + ) -> Result<(), EngineError> { + let yara_scanner = YaraScanner::new()?; + let yara_matches = + yara_scanner.scan(ctx.data())?; + + let format_result = ctx.format_result.as_ref(); + let import_result = ctx.import_result.as_ref(); + let string_result = ctx.string_result.as_ref(); + let entropy_result = + ctx.entropy_result.as_ref(); + + let result = compute_threat_score( + format_result, + import_result, + string_result, + entropy_result, + &yara_matches, + ); + ctx.threat_result = Some(result); + Ok(()) + } +} + +pub fn compute_threat_score( + format_result: Option<&FormatResult>, + import_result: Option<&ImportResult>, + string_result: Option<&StringResult>, + entropy_result: Option<&EntropyResult>, + yara_matches: &[YaraMatch], +) -> ThreatResult { + let cat_import = + score_imports(import_result, string_result); + let cat_entropy = score_entropy(entropy_result); + let cat_packing = score_packing( + entropy_result, + format_result, + string_result, + ); + let cat_strings = score_strings(string_result); + let cat_sections = score_sections(format_result); + let cat_ep = score_entry_point(format_result); + let cat_anti = score_anti_analysis( + import_result, + string_result, + ); + let cat_yara = score_yara(yara_matches); + + let categories = vec![ + cat_import, + cat_entropy, + cat_packing, + cat_strings, + cat_sections, + cat_ep, + cat_anti, + cat_yara, + ]; + + let total_score: u32 = categories + .iter() + .map(|c| c.score.min(c.max_score)) + .sum(); + + let risk_level = classify_risk(total_score); + + let mut mitre_techniques = Vec::new(); + if let Some(ir) = import_result { + for combo in &ir.suspicious_combinations { + if !combo.mitre_id.is_empty() { + mitre_techniques.push(MitreMapping { + technique_id: combo + .mitre_id + .clone(), + technique_name: combo + .name + .clone(), + tactic: combo + .description + .clone(), + evidence: combo + .apis + .join(" + "), + }); + } + } + for mapping in &ir.mitre_mappings { + mitre_techniques.push(MitreMapping { + technique_id: mapping + .technique_id + .clone(), + technique_name: mapping.tag.clone(), + tactic: mapping.tag.clone(), + evidence: mapping.api.clone(), + }); + } + } + + let mut seen_techniques = HashSet::new(); + mitre_techniques.retain(|t| { + seen_techniques.insert(t.technique_id.clone()) + }); + + let summary = generate_summary( + &categories, + total_score, + &risk_level, + ); + + ThreatResult { + total_score, + risk_level, + categories, + mitre_techniques, + yara_matches: yara_matches.to_vec(), + summary, + } +} + +pub fn classify_risk(score: u32) -> RiskLevel { + match score { + 0..=BENIGN_MAX => RiskLevel::Benign, + 16..=LOW_MAX => RiskLevel::Low, + 36..=MEDIUM_MAX => RiskLevel::Medium, + 56..=HIGH_MAX => RiskLevel::High, + _ => RiskLevel::Critical, + } +} + +fn score_imports( + import_result: Option<&ImportResult>, + string_result: Option<&StringResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(ir) = import_result { + for combo in &ir.suspicious_combinations { + let points = match combo.name.as_str() { + "Process Injection Chain" => { + INJECTION_CHAIN + } + "Process Hollowing" => { + HOLLOWING_CHAIN + } + "Credential Theft" => { + CREDENTIAL_ACCESS + } + "APC Injection" => APC_INJECTION, + "DLL Injection" => ANTI_DEBUG_API, + "Download and Execute" + | "Download and Execute (WinInet)" => { + NETWORK_DOWNLOAD + } + "Registry Persistence" => { + REGISTRY_RUN_KEYS + } + "Service Persistence" => { + REGISTRY_RUN_KEYS + } + "Linux ptrace Injection" => { + LINUX_PTRACE + } + "Linux RWX Memory" => { + LINUX_RWX_MEMORY + } + "Linux C2 Connection" => { + LINUX_C2_CONNECTION + } + "Linux Network Listener" => { + LINUX_NETWORK_LISTENER + } + "Linux Dynamic Loading" => { + LINUX_DYNAMIC_LOADING + } + "Linux Process Injection" => { + LINUX_PROCESS_INJECTION + } + _ => 3, + }; + details.push(ScoringDetail { + rule: format!( + "{} detected", + combo.name + ), + points, + evidence: combo + .apis + .join(" + "), + }); + raw += points; + } + + for imp in &ir.imports { + if imp.is_suspicious + && imp + .threat_tags + .iter() + .any(|t| t == "anti-debug") + { + let pts = if imp.function + == "NtQueryInformationProcess" + { + ANTI_DEBUG_API + } else { + 3 + }; + details.push(ScoringDetail { + rule: format!( + "Anti-debug API: {}", + imp.function + ), + points: pts, + evidence: imp.function.clone(), + }); + raw += pts; + } + } + + if ir.statistics.total_imports + < FEW_IMPORTS_THRESHOLD + && ir.statistics.total_imports > 0 + { + details.push(ScoringDetail { + rule: "Very few imports".into(), + points: VERY_FEW_IMPORTS, + evidence: format!( + "{} imports", + ir.statistics.total_imports + ), + }); + raw += VERY_FEW_IMPORTS; + } + } + + if let Some(sr) = string_result { + let import_names: HashSet<&str> = import_result + .map(|ir| { + ir.imports + .iter() + .map(|i| i.function.as_str()) + .collect() + }) + .unwrap_or_default(); + let string_only_apis = sr + .strings + .iter() + .filter(|s| { + s.category + == StringCategory::SuspiciousApi + && !import_names + .contains(s.value.as_str()) + }) + .count(); + if string_only_apis > 0 { + details.push(ScoringDetail { + rule: "Suspicious API strings (not imported)".into(), + points: 3, + evidence: format!( + "{string_only_apis} API names found in strings only" + ), + }); + raw += 3; + } + } + + ScoringCategory { + name: "Import/API Analysis".into(), + score: raw.min(IMPORT_MAX), + max_score: IMPORT_MAX, + details, + } +} + +fn score_entropy( + entropy_result: Option<&EntropyResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(er) = entropy_result { + let mut high_count = 0u32; + for section in &er.sections { + if section.entropy > HIGH_ENTROPY_THRESHOLD + && high_count < HIGH_SECTION_ENTROPY_CAP + { + details.push(ScoringDetail { + rule: format!( + "High entropy section: {}", + section.name + ), + points: HIGH_SECTION_ENTROPY, + evidence: format!( + "entropy={:.2}", + section.entropy + ), + }); + raw += HIGH_SECTION_ENTROPY; + high_count += 1; + } + } + + if er.overall_entropy + > VERY_HIGH_ENTROPY_THRESHOLD + { + details.push(ScoringDetail { + rule: "Very high overall entropy" + .into(), + points: VERY_HIGH_OVERALL_ENTROPY, + evidence: format!( + "overall={:.2}", + er.overall_entropy + ), + }); + raw += VERY_HIGH_OVERALL_ENTROPY; + } + } + + ScoringCategory { + name: "Entropy Analysis".into(), + score: raw.min(ENTROPY_MAX), + max_score: ENTROPY_MAX, + details, + } +} + +fn score_packing( + entropy_result: Option<&EntropyResult>, + format_result: Option<&FormatResult>, + string_result: Option<&StringResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(er) = entropy_result { + let mut has_packer_section = false; + for section in &er.sections { + if section + .flags + .contains(&EntropyFlag::PackerSectionName) + { + has_packer_section = true; + details.push(ScoringDetail { + rule: "Known packer section name" + .into(), + points: PACKER_SECTION_NAME, + evidence: section.name.clone(), + }); + raw += PACKER_SECTION_NAME; + } + if section + .flags + .contains(&EntropyFlag::EmptyRawData) + { + details.push(ScoringDetail { + rule: + "Empty raw with virtual size" + .into(), + points: EMPTY_RAW_WITH_VIRTUAL, + evidence: format!( + "section={} virtual={}", + section.name, + section.virtual_to_raw_ratio + ), + }); + raw += EMPTY_RAW_WITH_VIRTUAL; + } + if section.flags.contains( + &EntropyFlag::HighVirtualToRawRatio, + ) { + details.push(ScoringDetail { + rule: "High virtual/raw ratio" + .into(), + points: HIGH_VR_RATIO, + evidence: format!( + "section={} ratio={:.1}", + section.name, + section.virtual_to_raw_ratio + ), + }); + raw += HIGH_VR_RATIO; + } + } + + if let Some(packer) = &er.packer_name { + details.push(ScoringDetail { + rule: "Packer signature match".into(), + points: PACKER_SIGNATURE_MATCH, + evidence: packer.clone(), + }); + raw += PACKER_SIGNATURE_MATCH; + } + + let has_pushad = er + .packing_indicators + .iter() + .any(|pi| { + pi.indicator_type == "entry_point" + }); + if has_pushad { + details.push(ScoringDetail { + rule: "PUSHAD at entry point".into(), + points: PUSHAD_EP, + evidence: "0x60 at EP".into(), + }); + raw += PUSHAD_EP; + } + + let has_upx_sections = er + .packing_indicators + .iter() + .any(|pi| { + pi.packer_name.as_deref() + == Some("UPX") + }); + let has_upx_magic = string_result + .map_or(false, |sr| { + sr.strings.iter().any(|s| { + s.value.contains("UPX!") + }) + }); + if has_packer_section + && has_upx_sections + && !has_upx_magic + { + details.push(ScoringDetail { + rule: "Modified UPX".into(), + points: MODIFIED_UPX, + evidence: "UPX sections without UPX! magic".into(), + }); + raw += MODIFIED_UPX; + } + } + + if let Some(fr) = format_result { + let suspicious_section_count = + fr.sections.iter().filter(|s| { + crate::formats::SUSPICIOUS_SECTION_NAMES + .iter() + .any(|&(sus, _)| s.name == sus) + }).count(); + if suspicious_section_count > 0 { + details.push(ScoringDetail { + rule: "Suspicious section names".into(), + points: 3, + evidence: format!( + "{suspicious_section_count} suspicious section names" + ), + }); + raw += 3; + } + } + + ScoringCategory { + name: "Packing Indicators".into(), + score: raw.min(PACKING_MAX), + max_score: PACKING_MAX, + details, + } +} + +fn score_strings( + string_result: Option<&StringResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(sr) = string_result { + let has_suspicious_urls = + sr.strings.iter().any(|s| { + s.category == StringCategory::Url + && SUSPICIOUS_TLDS.iter().any( + |tld| { + s.value + .to_ascii_lowercase() + .contains(tld) + }, + ) + }); + if has_suspicious_urls { + details.push(ScoringDetail { + rule: "C2/malicious URL patterns" + .into(), + points: C2_PATTERN, + evidence: "URL with suspicious TLD" + .into(), + }); + raw += C2_PATTERN; + } + + let has_shell_commands = sr + .strings + .iter() + .any(|s| { + s.category == StringCategory::ShellCommand + }); + if has_shell_commands { + details.push(ScoringDetail { + rule: "Suspicious shell commands" + .into(), + points: SUSPICIOUS_COMMANDS, + evidence: "Shell command strings found" + .into(), + }); + raw += SUSPICIOUS_COMMANDS; + } + + let has_base64_pe = sr.strings.iter().any( + |s| { + s.category == StringCategory::EncodedData + && BASE64_MZ_PREFIXES.iter().any( + |prefix| { + s.value.starts_with(prefix) + }, + ) + }, + ); + if has_base64_pe { + details.push(ScoringDetail { + rule: "Base64-encoded PE header" + .into(), + points: BASE64_PE_HEADER, + evidence: "TVqQ/TVpQ prefix in Base64" + .into(), + }); + raw += BASE64_PE_HEADER; + } + + let has_reg_persistence = sr + .strings + .iter() + .any(|s| { + s.category + == StringCategory::PersistencePath + }); + if has_reg_persistence { + details.push(ScoringDetail { + rule: "Registry persistence paths" + .into(), + points: REGISTRY_PERSISTENCE, + evidence: + "Run/RunOnce registry paths found" + .into(), + }); + raw += REGISTRY_PERSISTENCE; + } + + let has_crypto_wallets = sr + .strings + .iter() + .any(|s| { + s.category + == StringCategory::CryptoWallet + }); + if has_crypto_wallets { + details.push(ScoringDetail { + rule: "Crypto wallet addresses".into(), + points: CRYPTO_WALLET, + evidence: "BTC/ETH address patterns" + .into(), + }); + raw += CRYPTO_WALLET; + } + } + + ScoringCategory { + name: "String Analysis".into(), + score: raw.min(STRING_MAX), + max_score: STRING_MAX, + details, + } +} + +fn score_sections( + format_result: Option<&FormatResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(fr) = format_result { + let has_rwx = fr.anomalies.iter().any(|a| { + matches!( + a, + FormatAnomaly::RwxSection { .. } + ) + }); + if has_rwx { + details.push(ScoringDetail { + rule: "RWX section detected".into(), + points: RWX_SECTION, + evidence: "Read+Write+Execute section" + .into(), + }); + raw += RWX_SECTION; + } + + let has_empty_name = + fr.anomalies.iter().any(|a| { + matches!( + a, + FormatAnomaly::EmptySectionName { + .. + } + ) + }); + if has_empty_name { + details.push(ScoringDetail { + rule: "Empty/null section name".into(), + points: EMPTY_SECTION_NAME, + evidence: + "Section with empty or null name" + .into(), + }); + raw += EMPTY_SECTION_NAME; + } + + let section_count = fr.sections.len(); + if section_count > MAX_NORMAL_SECTIONS + || section_count == 0 + { + details.push(ScoringDetail { + rule: "Unusual section count".into(), + points: UNUSUAL_SECTION_COUNT, + evidence: format!( + "{section_count} sections" + ), + }); + raw += UNUSUAL_SECTION_COUNT; + } + + let has_zero_code = fr.sections.iter().any( + |s| { + (s.name == ".text" || s.name == ".code") + && s.raw_size == 0 + }, + ); + if has_zero_code { + details.push(ScoringDetail { + rule: "Zero-size code section".into(), + points: ZERO_SIZE_CODE, + evidence: + ".text/.code with raw_size == 0" + .into(), + }); + raw += ZERO_SIZE_CODE; + } + } + + ScoringCategory { + name: "Section Anomalies".into(), + score: raw.min(SECTION_MAX), + max_score: SECTION_MAX, + details, + } +} + +fn score_entry_point( + format_result: Option<&FormatResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + if let Some(fr) = format_result { + let ep = fr.entry_point; + + let ep_section = fr.sections.iter().find(|s| { + ep >= s.virtual_address + && ep < s.virtual_address + + s.virtual_size + }); + + let in_text = fr.sections.iter().any(|s| { + s.name == ".text" + && ep >= s.virtual_address + && ep < s.virtual_address + + s.virtual_size + }); + + if !in_text && ep_section.is_some() { + details.push(ScoringDetail { + rule: "EP outside .text section" + .into(), + points: EP_OUTSIDE_TEXT, + evidence: format!( + "EP=0x{ep:x} not in .text" + ), + }); + raw += EP_OUTSIDE_TEXT; + } + + if ep_section.is_none() && !fr.sections.is_empty() + { + details.push(ScoringDetail { + rule: "EP outside all sections".into(), + points: EP_OUTSIDE_ALL, + evidence: format!( + "EP=0x{ep:x} not in any section" + ), + }); + raw += EP_OUTSIDE_ALL; + } + + if let Some(last) = fr.sections.last() { + if ep >= last.virtual_address + && ep < last.virtual_address + + last.virtual_size + && fr.sections.len() > 1 + { + details.push(ScoringDetail { + rule: "EP in last section".into(), + points: EP_LAST_SECTION, + evidence: format!( + "EP=0x{ep:x} in last section '{}'", + last.name + ), + }); + raw += EP_LAST_SECTION; + } + } + + let has_tls = fr.anomalies.iter().any(|a| { + matches!( + a, + FormatAnomaly::TlsCallbacksPresent { + .. + } + ) + }); + if has_tls { + details.push(ScoringDetail { + rule: "TLS callbacks present".into(), + points: TLS_CALLBACKS, + evidence: "PE TLS callback entries" + .into(), + }); + raw += TLS_CALLBACKS; + } + } + + ScoringCategory { + name: "Entry Point Anomalies".into(), + score: raw.min(ENTRY_POINT_MAX), + max_score: ENTRY_POINT_MAX, + details, + } +} + +fn score_anti_analysis( + import_result: Option<&ImportResult>, + string_result: Option<&StringResult>, +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + let all_names = collect_api_and_string_names( + import_result, + string_result, + ); + + if all_names.iter().any(|n| n == "IsDebuggerPresent") + { + details.push(ScoringDetail { + rule: "IsDebuggerPresent detected".into(), + points: IS_DEBUGGER_PRESENT, + evidence: "IsDebuggerPresent".into(), + }); + raw += IS_DEBUGGER_PRESENT; + } + + if all_names + .iter() + .any(|n| n == "NtQueryInformationProcess") + { + details.push(ScoringDetail { + rule: "NtQueryInformationProcess detected" + .into(), + points: NT_QUERY_INFO_PROCESS, + evidence: "NtQueryInformationProcess" + .into(), + }); + raw += NT_QUERY_INFO_PROCESS; + } + + if let Some(sr) = string_result { + let has_vm = sr.strings.iter().any(|s| { + let lower = s.value.to_ascii_lowercase(); + VM_STRINGS + .iter() + .any(|vm| lower.contains(vm)) + }); + if has_vm { + details.push(ScoringDetail { + rule: "VM detection strings".into(), + points: VM_DETECTION_STRINGS, + evidence: + "VMware/VBox/QEMU/Hyper-V strings" + .into(), + }); + raw += VM_DETECTION_STRINGS; + } + + let has_sandbox = sr.strings.iter().any(|s| { + let lower = s.value.to_ascii_lowercase(); + SANDBOX_STRINGS + .iter() + .any(|sb| lower.contains(sb)) + }); + if has_sandbox { + details.push(ScoringDetail { + rule: "Sandbox evasion strings".into(), + points: SANDBOX_EVASION, + evidence: + "sandbox/cuckoo/wireshark strings" + .into(), + }); + raw += SANDBOX_EVASION; + } + + let has_linux_anti_debug = + sr.strings.iter().any(|s| { + s.category + == StringCategory::AntiAnalysis + && s.value.contains("TracerPid") + }); + if has_linux_anti_debug { + details.push(ScoringDetail { + rule: "Linux ptrace anti-debug" + .into(), + points: LINUX_PTRACE_CHECK, + evidence: + "TracerPid check detected" + .into(), + }); + raw += LINUX_PTRACE_CHECK; + } + + let has_proc_analysis = + sr.strings.iter().any(|s| { + s.category + == StringCategory::AntiAnalysis + && (s.value + .contains("/proc/self/maps") + || s.value.contains( + "/proc/self/status", + )) + }); + if has_proc_analysis { + details.push(ScoringDetail { + rule: "/proc/self analysis".into(), + points: PROC_SELF_ANALYSIS, + evidence: + "Process self-inspection detected" + .into(), + }); + raw += PROC_SELF_ANALYSIS; + } + } + + let has_timing = all_names.iter().any(|n| { + TIMING_CHECK_FUNCTIONS + .iter() + .any(|t| n.contains(t)) + }); + if has_timing { + details.push(ScoringDetail { + rule: "Timing check APIs".into(), + points: TIMING_CHECK_APIS, + evidence: "GetTickCount/QueryPerformanceCounter".into(), + }); + raw += TIMING_CHECK_APIS; + } + + ScoringCategory { + name: "Anti-Analysis Indicators".into(), + score: raw.min(ANTI_ANALYSIS_MAX), + max_score: ANTI_ANALYSIS_MAX, + details, + } +} + +fn score_yara( + yara_matches: &[YaraMatch], +) -> ScoringCategory { + let mut details = Vec::new(); + let mut raw = 0u32; + + for ym in yara_matches { + let category = ym + .metadata + .category + .as_deref() + .unwrap_or(""); + let severity = ym + .metadata + .severity + .as_deref() + .unwrap_or(""); + + let points = if category == "malware" + || severity == "critical" + { + YARA_MALWARE_FAMILY + } else if category == "packer" { + YARA_PACKER_RULE + } else if category == "c2" + || category == "credential-access" + { + YARA_SUSPICIOUS + 2 + } else { + YARA_SUSPICIOUS + }; + + details.push(ScoringDetail { + rule: format!( + "YARA: {}", + ym.rule_name + ), + points, + evidence: ym + .metadata + .description + .clone() + .unwrap_or_default(), + }); + raw += points; + } + + ScoringCategory { + name: "YARA Signature Matches".into(), + score: raw.min(YARA_MAX), + max_score: YARA_MAX, + details, + } +} + +fn collect_api_and_string_names( + import_result: Option<&ImportResult>, + string_result: Option<&StringResult>, +) -> Vec { + let mut names = Vec::new(); + if let Some(ir) = import_result { + for imp in &ir.imports { + names.push(imp.function.clone()); + } + } + if let Some(sr) = string_result { + for s in &sr.strings { + if s.category + == StringCategory::SuspiciousApi + { + names.push(s.value.clone()); + } + } + } + names +} + +fn generate_summary( + categories: &[ScoringCategory], + total_score: u32, + risk_level: &RiskLevel, +) -> String { + let mut all_details: Vec<(&str, &ScoringDetail)> = + Vec::new(); + for cat in categories { + for detail in &cat.details { + all_details + .push((&cat.name, detail)); + } + } + all_details + .sort_by(|a, b| b.1.points.cmp(&a.1.points)); + + let top: Vec = all_details + .iter() + .take(SUMMARY_TOP_N) + .map(|(_, d)| d.rule.clone()) + .collect(); + + if top.is_empty() { + return format!( + "{risk_level} risk (score {total_score}/100): \ + No significant threat indicators detected" + ); + } + + format!( + "{risk_level} risk (score {total_score}/100): {}", + top.join(", ") + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn risk_level_classification() { + assert_eq!(classify_risk(0), RiskLevel::Benign); + assert_eq!( + classify_risk(15), + RiskLevel::Benign + ); + assert_eq!(classify_risk(16), RiskLevel::Low); + assert_eq!(classify_risk(35), RiskLevel::Low); + assert_eq!( + classify_risk(36), + RiskLevel::Medium + ); + assert_eq!( + classify_risk(55), + RiskLevel::Medium + ); + assert_eq!(classify_risk(56), RiskLevel::High); + assert_eq!(classify_risk(75), RiskLevel::High); + assert_eq!( + classify_risk(76), + RiskLevel::Critical + ); + assert_eq!( + classify_risk(100), + RiskLevel::Critical + ); + } + + #[test] + fn category_capping() { + let cat = ScoringCategory { + name: "Test".into(), + score: 30, + max_score: 20, + details: Vec::new(), + }; + assert_eq!(cat.score.min(cat.max_score), 20); + } + + #[test] + fn total_is_sum_of_capped() { + let result = compute_threat_score( + None, None, None, None, &[], + ); + assert_eq!(result.total_score, 0); + assert_eq!( + result.risk_level, + RiskLevel::Benign + ); + } + + #[test] + fn summary_empty_when_no_threats() { + let result = compute_threat_score( + None, None, None, None, &[], + ); + assert!(result.summary.contains("BENIGN")); + assert!(result.summary.contains("0/100")); + } + + #[test] + fn yara_scoring_malware() { + let matches = vec![YaraMatch { + rule_name: "test_malware".into(), + tags: Vec::new(), + metadata: crate::yara::YaraMetadata { + description: Some( + "Test malware rule".into(), + ), + category: Some("malware".into()), + severity: Some("critical".into()), + }, + matched_strings: Vec::new(), + }]; + let cat = score_yara(&matches); + assert_eq!(cat.score, YARA_MALWARE_FAMILY); + } + + #[test] + fn yara_scoring_packer() { + let matches = vec![YaraMatch { + rule_name: "test_packer".into(), + tags: Vec::new(), + metadata: crate::yara::YaraMetadata { + description: Some( + "Test packer rule".into(), + ), + category: Some("packer".into()), + severity: Some("medium".into()), + }, + matched_strings: Vec::new(), + }]; + let cat = score_yara(&matches); + assert_eq!(cat.score, YARA_PACKER_RULE); + } + + #[test] + fn entropy_scoring() { + use crate::passes::entropy::{ + EntropyResult, SectionEntropy, + }; + use crate::types::EntropyClassification; + + let er = EntropyResult { + overall_entropy: 7.2, + sections: vec![SectionEntropy { + name: ".text".into(), + entropy: 7.5, + size: 4096, + classification: + EntropyClassification::Encrypted, + virtual_to_raw_ratio: 1.0, + is_anomalous: true, + flags: vec![EntropyFlag::HighEntropy], + }], + packing_detected: false, + packer_name: None, + packing_indicators: Vec::new(), + }; + let cat = score_entropy(Some(&er)); + assert!( + cat.score > 0, + "should score for high entropy" + ); + assert!(cat.details.len() >= 2); + } + + #[test] + fn section_rwx_scoring() { + use crate::formats::{ + FormatResult, SectionInfo, + }; + use crate::types::{ + Architecture, BinaryFormat, Endianness, + SectionPermissions, + }; + + let fr = FormatResult { + format: BinaryFormat::Elf, + architecture: Architecture::X86_64, + bits: 64, + endianness: Endianness::Little, + entry_point: 0x1000, + is_stripped: false, + is_pie: false, + has_debug_info: false, + sections: vec![SectionInfo { + name: ".text".into(), + virtual_address: 0x1000, + virtual_size: 0x1000, + raw_offset: 0, + raw_size: 0x1000, + permissions: SectionPermissions { + read: true, + write: true, + execute: true, + }, + sha256: String::new(), + }], + segments: Vec::new(), + anomalies: vec![ + FormatAnomaly::RwxSection { + name: ".text".into(), + }, + ], + pe_info: None, + elf_info: None, + macho_info: None, + function_hints: Vec::new(), + }; + let cat = score_sections(Some(&fr)); + assert_eq!(cat.score, RWX_SECTION); + } + + #[test] + fn threat_pass_populates_context() { + use std::sync::Arc; + use crate::context::BinarySource; + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else( + |e| panic!("fixture {path}: {e}"), + ) + } + + let data = load_fixture("hello_elf"); + let size = data.len() as u64; + let mut ctx = AnalysisContext::new( + BinarySource::Buffered(Arc::from(data)), + "deadbeef".into(), + "test.bin".into(), + size, + ); + + crate::passes::format::FormatPass + .run(&mut ctx) + .unwrap(); + crate::passes::imports::ImportPass + .run(&mut ctx) + .unwrap(); + crate::passes::strings::StringPass + .run(&mut ctx) + .unwrap(); + crate::passes::entropy::EntropyPass + .run(&mut ctx) + .unwrap(); + crate::passes::disasm::DisasmPass + .run(&mut ctx) + .unwrap(); + + assert!(ctx.threat_result.is_none()); + ThreatPass.run(&mut ctx).unwrap(); + assert!(ctx.threat_result.is_some()); + + let result = ctx.threat_result.unwrap(); + assert_eq!(result.categories.len(), 8); + assert!(result.total_score <= 100); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs new file mode 100644 index 0000000..61337cb --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs @@ -0,0 +1,161 @@ +// ©AngelaMos | 2026 +// types.rs + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum BinaryFormat { + Elf, + Pe, + MachO, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Architecture { + X86, + X86_64, + Arm, + Aarch64, + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Endianness { + Little, + Big, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RiskLevel { + Benign, + Low, + Medium, + High, + Critical, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Severity { + Low, + Medium, + High, + Critical, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum StringEncoding { + Ascii, + Utf8, + Utf16Le, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum StringCategory { + Url, + IpAddress, + FilePath, + RegistryKey, + ShellCommand, + CryptoWallet, + Email, + SuspiciousApi, + PackerSignature, + DebugArtifact, + AntiAnalysis, + PersistencePath, + EncodedData, + Generic, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum EntropyClassification { + Plaintext, + NativeCode, + Compressed, + Packed, + Encrypted, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum EntropyFlag { + HighEntropy, + HighVirtualToRawRatio, + EmptyRawData, + Rwx, + PackerSectionName, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum FlowControlType { + Next, + Branch, + ConditionalBranch, + Call, + Return, + Interrupt, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CfgEdgeType { + Fallthrough, + ConditionalTrue, + ConditionalFalse, + Unconditional, + Call, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SectionPermissions { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl std::fmt::Display for BinaryFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Elf => write!(f, "ELF"), + Self::Pe => write!(f, "PE"), + Self::MachO => write!(f, "Mach-O"), + } + } +} + +impl std::fmt::Display for Architecture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::X86 => write!(f, "x86"), + Self::X86_64 => write!(f, "x86_64"), + Self::Arm => write!(f, "ARM"), + Self::Aarch64 => write!(f, "AArch64"), + Self::Other(name) => write!(f, "{name}"), + } + } +} + +impl std::fmt::Display for RiskLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Benign => write!(f, "BENIGN"), + Self::Low => write!(f, "LOW"), + Self::Medium => write!(f, "MEDIUM"), + Self::High => write!(f, "HIGH"), + Self::Critical => write!(f, "CRITICAL"), + } + } +} + +impl std::fmt::Display for Endianness { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Little => write!(f, "Little-endian"), + Self::Big => write!(f, "Big-endian"), + } + } +} + +impl SectionPermissions { + pub fn is_rwx(&self) -> bool { + self.read && self.write && self.execute + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs new file mode 100644 index 0000000..39232cb --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs @@ -0,0 +1,493 @@ +// ©AngelaMos | 2026 +// yara.rs + +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::error::EngineError; + +const BUILTIN_RULES: &str = r#" +rule suspicious_upx_packed { + meta: + description = "Detects UPX packed binaries" + category = "packer" + severity = "medium" + strings: + $upx0 = "UPX0" + $upx1 = "UPX1" + $upx_magic = { 55 50 58 21 } + condition: + ($upx0 and $upx1) or $upx_magic +} + +rule suspicious_anti_debug { + meta: + description = "Detects common anti-debugging techniques" + category = "evasion" + severity = "high" + strings: + $api1 = "IsDebuggerPresent" + $api2 = "CheckRemoteDebuggerPresent" + $api3 = "NtQueryInformationProcess" + $api4 = "OutputDebugString" + $int2d = { CD 2D } + condition: + 2 of ($api*) or $int2d +} + +rule suspicious_process_injection { + meta: + description = "Detects potential process injection capabilities" + category = "injection" + severity = "critical" + strings: + $api1 = "VirtualAllocEx" + $api2 = "WriteProcessMemory" + $api3 = "CreateRemoteThread" + $api4 = "NtUnmapViewOfSection" + condition: + ($api1 and $api2 and $api3) or ($api4 and $api2) +} + +rule suspicious_keylogger { + meta: + description = "Detects potential keylogger behavior" + category = "spyware" + severity = "high" + strings: + $api1 = "GetAsyncKeyState" + $api2 = "SetWindowsHookEx" + $api3 = "GetKeyState" + $api4 = "GetKeyboardState" + condition: + 2 of them +} + +rule suspicious_crypto_mining { + meta: + description = "Detects cryptocurrency mining indicators" + category = "miner" + severity = "medium" + strings: + $pool1 = "stratum+tcp://" + $pool2 = "stratum+ssl://" + $algo1 = "cryptonight" + $algo2 = "randomx" + $algo3 = "ethash" + $wallet = /[13][a-km-zA-HJ-NP-Z1-9]{25,34}/ + condition: + any of ($pool*) or (any of ($algo*) and $wallet) +} + +rule suspicious_persistence { + meta: + description = "Detects Windows persistence mechanisms" + category = "persistence" + severity = "high" + strings: + $reg1 = "CurrentVersion\\Run" + $reg2 = "CurrentVersion\\RunOnce" + $svc1 = "CreateServiceA" + $svc2 = "CreateServiceW" + $task = "schtasks" + condition: + any of them +} + +rule suspicious_network_backdoor { + meta: + description = "Detects potential backdoor network behavior" + category = "backdoor" + severity = "critical" + strings: + $bind = "bind" + $listen = "listen" + $accept = "accept" + $shell1 = "cmd.exe" + $shell2 = "/bin/sh" + $shell3 = "/bin/bash" + condition: + ($bind and $listen and $accept) and any of ($shell*) +} + +rule suspicious_ransomware { + meta: + description = "Detects potential ransomware indicators" + category = "ransomware" + severity = "critical" + strings: + $ext1 = ".encrypted" + $ext2 = ".locked" + $ext3 = ".crypto" + $ransom1 = "your files have been encrypted" + $ransom2 = "bitcoin" + $ransom3 = "decrypt" + $crypto1 = "CryptEncrypt" + $crypto2 = "CryptGenKey" + condition: + (any of ($ext*) and any of ($ransom*)) or + (any of ($crypto*) and any of ($ransom*)) +} + +rule suspicious_shellcode { + meta: + description = "Detects potential shellcode patterns" + category = "shellcode" + severity = "high" + strings: + $nop_sled = { 90 90 90 90 90 90 90 90 } + $egg_hunter1 = { 66 81 CA FF 0F } + $stack_pivot = { 94 C3 } + condition: + any of them +} + +rule suspicious_obfuscation { + meta: + description = "Detects common obfuscation patterns" + category = "obfuscation" + severity = "medium" + strings: + $xor_loop = { 80 3? ?? 74 ?? 80 3? ?? } + $decode_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + condition: + any of them +} + +rule suspicious_linux_anti_debug { + meta: + description = "Detects Linux anti-debugging via /proc inspection" + category = "evasion" + severity = "high" + strings: + $tracer = "TracerPid" + $proc_status = "/proc/self/status" + $proc_maps = "/proc/self/maps" + condition: + $tracer or ($proc_status and $proc_maps) +} + +rule suspicious_linux_persistence { + meta: + description = "Detects Linux persistence mechanisms" + category = "persistence" + severity = "high" + strings: + $cron1 = "/etc/cron" + $cron2 = "crontab" + $init = "/etc/init.d/" + $systemd = "/etc/systemd/" + $bashrc = ".bashrc" + $profile = ".bash_profile" + $rc_local = "/etc/rc.local" + $xdg_autostart = ".config/autostart" + condition: + 2 of them +} + +rule suspicious_c2_endpoints { + meta: + description = "Detects common C2 server endpoint paths" + category = "c2" + severity = "high" + strings: + $gate = "/gate.php" + $beacon = "/beacon" + $callback = "/callback" + $checkin = "/checkin" + $exfil = "/exfil" + $panel = "/panel/" + $command = "/command" + $bot = "/bot/" + $upload_php = "/upload.php" + condition: + 2 of them +} + +rule suspicious_credential_access { + meta: + description = "Detects credential file access patterns" + category = "credential-access" + severity = "high" + strings: + $passwd = "/etc/passwd" + $shadow = "/etc/shadow" + $ssh_key = ".ssh/id_rsa" + $ssh_key2 = ".ssh/authorized_keys" + $kerberos = "/etc/krb5.conf" + $gnupg = ".gnupg/" + condition: + $shadow or ($passwd and any of ($ssh*, $kerberos, $gnupg)) +} +"#; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YaraMatch { + pub rule_name: String, + pub tags: Vec, + pub metadata: YaraMetadata, + pub matched_strings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YaraMetadata { + pub description: Option, + pub category: Option, + pub severity: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YaraStringMatch { + pub identifier: String, + pub match_count: usize, +} + +pub struct YaraScanner { + rules: yara_x::Rules, +} + +impl YaraScanner { + pub fn new() -> Result { + let mut compiler = yara_x::Compiler::new(); + compiler.add_source(BUILTIN_RULES).map_err( + |e| EngineError::Yara(e.to_string()), + )?; + + Ok(Self { + rules: compiler.build(), + }) + } + + pub fn with_custom_rules( + rules_dir: &Path, + ) -> Result { + let mut compiler = yara_x::Compiler::new(); + compiler.add_source(BUILTIN_RULES).map_err( + |e| EngineError::Yara(e.to_string()), + )?; + + if rules_dir.is_dir() { + for entry in std::fs::read_dir(rules_dir) + .map_err(|e| { + EngineError::Yara(format!( + "failed to read rules dir: {e}" + )) + })? + { + let entry = entry.map_err(|e| { + EngineError::Yara(format!( + "dir entry error: {e}" + )) + })?; + let path = entry.path(); + if path + .extension() + .is_some_and(|ext| ext == "yar" || ext == "yara") + { + let source = std::fs::read_to_string( + &path, + ) + .map_err(|e| { + EngineError::Yara(format!( + "failed to read {}: {e}", + path.display() + )) + })?; + compiler + .add_source(source.as_str()) + .map_err(|e| { + EngineError::Yara(format!( + "compile error in {}: {e}", + path.display() + )) + })?; + } + } + } + + Ok(Self { + rules: compiler.build(), + }) + } + + pub fn scan( + &self, + data: &[u8], + ) -> Result, EngineError> { + let mut scanner = + yara_x::Scanner::new(&self.rules); + let results = scanner.scan(data).map_err( + |e| EngineError::Yara(e.to_string()), + )?; + + let mut matches = Vec::new(); + for rule in results.matching_rules() { + let tags: Vec = rule + .tags() + .map(|t| t.identifier().to_string()) + .collect(); + + let mut description = None; + let mut category = None; + let mut severity = None; + for (key, value) in rule.metadata() { + match key { + "description" => { + if let yara_x::MetaValue::String(s) = value { + description = + Some(s.to_string()); + } + } + "category" => { + if let yara_x::MetaValue::String(s) = value { + category = + Some(s.to_string()); + } + } + "severity" => { + if let yara_x::MetaValue::String(s) = value { + severity = + Some(s.to_string()); + } + } + _ => {} + } + } + + let mut matched_strings = Vec::new(); + for pattern in rule.patterns() { + let id = pattern + .identifier() + .to_string(); + let count = + pattern.matches().count(); + if count > 0 { + matched_strings.push( + YaraStringMatch { + identifier: id, + match_count: count, + }, + ); + } + } + + matches.push(YaraMatch { + rule_name: rule + .identifier() + .to_string(), + tags, + metadata: YaraMetadata { + description, + category, + severity, + }, + matched_strings, + }); + } + + Ok(matches) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builtin_rules_compile() { + let scanner = YaraScanner::new().unwrap(); + let result = scanner.scan(&[0u8; 64]).unwrap(); + assert!(result.is_empty() || !result.is_empty()); + } + + #[test] + fn detects_upx_signature() { + let mut data = vec![0u8; 512]; + let upx0 = b"UPX0"; + let upx1 = b"UPX1"; + data[0x100..0x104].copy_from_slice(upx0); + data[0x140..0x144].copy_from_slice(upx1); + + let scanner = YaraScanner::new().unwrap(); + let result = scanner.scan(&data).unwrap(); + let upx_match = result + .iter() + .find(|m| m.rule_name == "suspicious_upx_packed"); + assert!( + upx_match.is_some(), + "should detect UPX packer signature" + ); + let meta = + &upx_match.unwrap().metadata; + assert_eq!( + meta.category.as_deref(), + Some("packer") + ); + } + + #[test] + fn detects_process_injection() { + let mut data = Vec::new(); + data.extend_from_slice( + b"\x00\x00VirtualAllocEx\x00\x00", + ); + data.extend_from_slice( + b"\x00\x00WriteProcessMemory\x00\x00", + ); + data.extend_from_slice( + b"\x00\x00CreateRemoteThread\x00\x00", + ); + data.extend_from_slice(&[0u8; 256]); + + let scanner = YaraScanner::new().unwrap(); + let result = scanner.scan(&data).unwrap(); + let injection = result.iter().find(|m| { + m.rule_name + == "suspicious_process_injection" + }); + assert!( + injection.is_some(), + "should detect process injection APIs" + ); + } + + #[test] + fn clean_data_no_matches() { + let data = b"Hello, this is perfectly normal text content with nothing suspicious at all."; + let scanner = YaraScanner::new().unwrap(); + let result = scanner.scan(data).unwrap(); + let suspicious: Vec<_> = result + .iter() + .filter(|m| { + m.rule_name != "suspicious_obfuscation" + }) + .collect(); + assert!( + suspicious.is_empty(), + "clean text should not trigger suspicious rules, got: {:?}", + suspicious.iter().map(|m| &m.rule_name).collect::>() + ); + } + + fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path).unwrap_or_else(|e| { + panic!("fixture {path}: {e}") + }) + } + + #[test] + fn scan_elf_binary() { + let data = load_fixture("hello_elf"); + let scanner = YaraScanner::new().unwrap(); + let result = scanner.scan(&data).unwrap(); + assert!( + result.is_empty() || !result.is_empty(), + "scan should complete without error" + ); + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf new file mode 100755 index 0000000..6d827e1 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf_stripped b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf_stripped new file mode 100755 index 0000000..e1d7ac0 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/fixtures/hello_elf_stripped differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs new file mode 100644 index 0000000..ab8d24f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs @@ -0,0 +1,93 @@ +// ©AngelaMos | 2026 +// integration.rs + +use axumortem_engine::types::BinaryFormat; +use axumortem_engine::AnalysisEngine; + +fn load_fixture(name: &str) -> Vec { + let path = format!( + "{}/tests/fixtures/{name}", + env!("CARGO_MANIFEST_DIR"), + ); + std::fs::read(&path) + .unwrap_or_else(|e| panic!("fixture {path}: {e}")) +} + +#[test] +fn full_pipeline_elf() { + let engine = AnalysisEngine::new().unwrap(); + let data = load_fixture("hello_elf"); + let (ctx, report) = + engine.analyze(&data, "hello_elf"); + + assert!( + report.all_succeeded(), + "all passes should succeed: {:?}", + report + .failed_passes() + .iter() + .map(|p| (p.name, p.error_message.as_deref())) + .collect::>() + ); + + let fmt = ctx.format_result.as_ref().unwrap(); + assert_eq!(fmt.format, BinaryFormat::Elf); + assert!(!fmt.sections.is_empty()); + + assert!(ctx.import_result.is_some()); + assert!(ctx.string_result.is_some()); + assert!(ctx.entropy_result.is_some()); + assert!(ctx.disassembly_result.is_some()); + + let disasm = + ctx.disassembly_result.as_ref().unwrap(); + assert!(disasm.total_functions > 0); + assert!(disasm.total_instructions > 0); + + let threat = ctx.threat_result.as_ref().unwrap(); + assert!(threat.total_score <= 100); + assert_eq!(threat.categories.len(), 8); + assert!(!threat.summary.is_empty()); +} + +#[test] +fn full_pipeline_stripped_elf() { + let engine = AnalysisEngine::new().unwrap(); + let data = load_fixture("hello_elf_stripped"); + let (ctx, report) = + engine.analyze(&data, "hello_elf_stripped"); + + assert!(report.all_succeeded()); + + let fmt = ctx.format_result.as_ref().unwrap(); + assert!(fmt.is_stripped); + + assert!(ctx.threat_result.is_some()); +} + +#[test] +fn sha256_computed_correctly() { + let engine = AnalysisEngine::new().unwrap(); + let data = load_fixture("hello_elf"); + let (ctx, _) = engine.analyze(&data, "test.bin"); + + assert_eq!(ctx.sha256.len(), 64); + assert!(ctx.sha256.chars().all(|c| c.is_ascii_hexdigit())); +} + +#[test] +fn invalid_binary_handled() { + let engine = AnalysisEngine::new().unwrap(); + let data = vec![0xDE, 0xAD, 0xBE, 0xEF]; + let (_, report) = + engine.analyze(&data, "garbage.bin"); + + assert!( + !report.all_succeeded(), + "invalid binary should cause format pass failure" + ); + assert!(report + .failed_passes() + .iter() + .any(|p| p.name == "format")); +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/Cargo.toml b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/Cargo.toml new file mode 100644 index 0000000..eff62dd --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/Cargo.toml @@ -0,0 +1,31 @@ +# ©AngelaMos | 2026 +# Cargo.toml + +[package] +name = "axumortem" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axumortem-engine = { path = "../axumortem-engine" } +axum = { version = "0.8", features = ["multipart"] } +tokio = { workspace = true } +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "postgres", + "uuid", + "chrono", + "json", + "migrate", +] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "trace", "limit"] } +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4", features = ["derive", "env"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/migrations/001_initial.sql b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/migrations/001_initial.sql new file mode 100644 index 0000000..cca71c2 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/migrations/001_initial.sql @@ -0,0 +1,31 @@ +-- ©AngelaMos | 2026 +-- 001_initial.sql + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +CREATE TABLE analyses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sha256 TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + file_size BIGINT NOT NULL, + format TEXT NOT NULL, + architecture TEXT NOT NULL, + entry_point BIGINT, + threat_score INTEGER, + risk_level TEXT, + slug TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE pass_results ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + analysis_id UUID NOT NULL REFERENCES analyses(id) ON DELETE CASCADE, + pass_name TEXT NOT NULL, + result JSONB NOT NULL, + duration_ms INTEGER, + UNIQUE(analysis_id, pass_name) +); + +CREATE INDEX idx_analyses_sha256 ON analyses(sha256); +CREATE INDEX idx_analyses_slug ON analyses(slug); +CREATE INDEX idx_pass_results_analysis_id ON pass_results(analysis_id); diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs new file mode 100644 index 0000000..1b3c60d --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs @@ -0,0 +1,33 @@ +// ©AngelaMos | 2026 +// config.rs + +use clap::Parser; + +const DEFAULT_HOST: &str = "0.0.0.0"; +const DEFAULT_PORT: u16 = 3000; +const DEFAULT_MAX_UPLOAD_BYTES: usize = 52_428_800; +const DEFAULT_CORS_ORIGIN: &str = "*"; + +#[derive(Parser, Debug)] +pub struct AppConfig { + #[arg(long, env = "DATABASE_URL")] + pub database_url: String, + + #[arg(long, env = "HOST", default_value = DEFAULT_HOST)] + pub host: String, + + #[arg(long, env = "PORT", default_value_t = DEFAULT_PORT)] + pub port: u16, + + #[arg(long, env = "MAX_UPLOAD_SIZE", default_value_t = DEFAULT_MAX_UPLOAD_BYTES)] + pub max_upload_size: usize, + + #[arg(long, env = "CORS_ORIGIN", default_value = DEFAULT_CORS_ORIGIN)] + pub cors_origin: String, +} + +impl AppConfig { + pub fn bind_address(&self) -> String { + format!("{}:{}", self.host, self.port) + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs new file mode 100644 index 0000000..3245184 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs @@ -0,0 +1,13 @@ +// ©AngelaMos | 2026 +// mod.rs + +pub mod models; +pub mod queries; + +use sqlx::PgPool; + +pub async fn run_migrations( + pool: &PgPool, +) -> Result<(), sqlx::migrate::MigrateError> { + sqlx::migrate!("./migrations").run(pool).await +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs new file mode 100644 index 0000000..5448329 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs @@ -0,0 +1,50 @@ +// ©AngelaMos | 2026 +// models.rs + +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(FromRow, Serialize)] +pub struct AnalysisRow { + pub id: Uuid, + pub sha256: String, + pub file_name: String, + pub file_size: i64, + pub format: String, + pub architecture: String, + pub entry_point: Option, + pub threat_score: Option, + pub risk_level: Option, + pub slug: String, + pub created_at: DateTime, +} + +#[derive(FromRow)] +pub struct PassResultRow { + pub id: Uuid, + pub analysis_id: Uuid, + pub pass_name: String, + pub result: serde_json::Value, + pub duration_ms: Option, +} + +pub struct NewAnalysis { + pub sha256: String, + pub file_name: String, + pub file_size: i64, + pub format: String, + pub architecture: String, + pub entry_point: Option, + pub threat_score: Option, + pub risk_level: Option, + pub slug: String, +} + +pub struct NewPassResult { + pub analysis_id: Uuid, + pub pass_name: String, + pub result: serde_json::Value, + pub duration_ms: Option, +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs new file mode 100644 index 0000000..5f24d41 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs @@ -0,0 +1,92 @@ +// ©AngelaMos | 2026 +// queries.rs + +use sqlx::{PgPool, Postgres, Transaction}; +use uuid::Uuid; + +use super::models::{ + AnalysisRow, NewAnalysis, NewPassResult, + PassResultRow, +}; + +pub async fn find_slug_by_sha256( + pool: &PgPool, + sha256: &str, +) -> Result, sqlx::Error> { + sqlx::query_scalar( + "SELECT slug FROM analyses WHERE sha256 = $1", + ) + .bind(sha256) + .fetch_optional(pool) + .await +} + +pub async fn find_by_slug( + pool: &PgPool, + slug: &str, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, AnalysisRow>( + "SELECT * FROM analyses WHERE slug = $1", + ) + .bind(slug) + .fetch_optional(pool) + .await +} + +pub async fn find_pass_results( + pool: &PgPool, + analysis_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, PassResultRow>( + "SELECT * FROM pass_results \ + WHERE analysis_id = $1 \ + ORDER BY pass_name", + ) + .bind(analysis_id) + .fetch_all(pool) + .await +} + +pub async fn insert_analysis( + tx: &mut Transaction<'_, Postgres>, + new: &NewAnalysis, +) -> Result { + sqlx::query_as::<_, AnalysisRow>( + "INSERT INTO analyses \ + (sha256, file_name, file_size, format, \ + architecture, entry_point, threat_score, \ + risk_level, slug) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \ + RETURNING *", + ) + .bind(&new.sha256) + .bind(&new.file_name) + .bind(new.file_size) + .bind(&new.format) + .bind(&new.architecture) + .bind(new.entry_point) + .bind(new.threat_score) + .bind(&new.risk_level) + .bind(&new.slug) + .fetch_one(tx.as_mut()) + .await +} + +pub async fn insert_pass_result( + tx: &mut Transaction<'_, Postgres>, + new: &NewPassResult, +) -> Result<(), sqlx::Error> { + sqlx::query( + "INSERT INTO pass_results \ + (analysis_id, pass_name, result, duration_ms) \ + VALUES ($1, $2, $3, $4)", + ) + .bind(new.analysis_id) + .bind(&new.pass_name) + .bind(&new.result) + .bind(new.duration_ms) + .execute(tx.as_mut()) + .await?; + + Ok(()) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs new file mode 100644 index 0000000..f209351 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs @@ -0,0 +1,100 @@ +// ©AngelaMos | 2026 +// error.rs + +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde::Serialize; + +#[derive(Serialize)] +struct ErrorBody { + error: ErrorDetail, +} + +#[derive(Serialize)] +struct ErrorDetail { + code: &'static str, + message: String, +} + +pub enum ApiError { + NoFile, + FileTooLarge { max_bytes: usize }, + InvalidBinary { reason: String }, + AnalysisFailed { reason: String }, + NotFound { resource: String }, + Internal { reason: String }, +} + +impl From for ApiError { + fn from(e: sqlx::Error) -> Self { + Self::Internal { + reason: e.to_string(), + } + } +} + +impl From for ApiError { + fn from(e: serde_json::Error) -> Self { + Self::Internal { + reason: e.to_string(), + } + } +} + +impl From for ApiError { + fn from(e: tokio::task::JoinError) -> Self { + Self::Internal { + reason: e.to_string(), + } + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, code, message) = match self { + Self::NoFile => ( + StatusCode::BAD_REQUEST, + "NO_FILE", + "No file was provided in the upload" + .to_string(), + ), + Self::FileTooLarge { max_bytes } => ( + StatusCode::BAD_REQUEST, + "FILE_TOO_LARGE", + format!( + "File exceeds maximum allowed size of {} bytes", + max_bytes + ), + ), + Self::InvalidBinary { reason } => ( + StatusCode::BAD_REQUEST, + "INVALID_BINARY", + reason, + ), + Self::AnalysisFailed { reason } => ( + StatusCode::INTERNAL_SERVER_ERROR, + "ANALYSIS_FAILED", + reason, + ), + Self::NotFound { resource } => ( + StatusCode::NOT_FOUND, + "NOT_FOUND", + format!("{resource} not found"), + ), + Self::Internal { reason } => ( + StatusCode::INTERNAL_SERVER_ERROR, + "INTERNAL", + reason, + ), + }; + + ( + status, + Json(ErrorBody { + error: ErrorDetail { code, message }, + }), + ) + .into_response() + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs new file mode 100644 index 0000000..4eea8f9 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs @@ -0,0 +1,95 @@ +// ©AngelaMos | 2026 +// main.rs + +mod config; +mod db; +mod error; +mod middleware; +mod routes; +mod state; + +use std::sync::Arc; + +use anyhow::Context; +use axum::extract::DefaultBodyLimit; +use clap::Parser; +use sqlx::postgres::PgPoolOptions; +use tower::ServiceBuilder; +use tower_http::trace::TraceLayer; +use tracing_subscriber::EnvFilter; + +use config::AppConfig; +use state::AppState; + +const DB_MAX_CONNECTIONS: u32 = 20; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let config = AppConfig::parse(); + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| { + EnvFilter::new( + "info,tower_http=debug", + ) + }), + ) + .init(); + + let db = PgPoolOptions::new() + .max_connections(DB_MAX_CONNECTIONS) + .connect(&config.database_url) + .await + .context("failed to connect to database")?; + + db::run_migrations(&db) + .await + .context("failed to run database migrations")?; + + let engine = axumortem_engine::AnalysisEngine::new() + .context( + "failed to initialize analysis engine", + )?; + + let config = Arc::new(config); + + let state = AppState { + db, + engine: Arc::new(engine), + config: Arc::clone(&config), + }; + + let layers = ServiceBuilder::new() + .layer(TraceLayer::new_for_http()) + .layer(middleware::cors::layer(&config)) + .layer(DefaultBodyLimit::max( + config.max_upload_size, + )); + + let app = + routes::api_router().layer(layers).with_state(state); + + let bind_address = config.bind_address(); + let listener = + tokio::net::TcpListener::bind(&bind_address) + .await + .context("failed to bind TCP listener")?; + + tracing::info!("listening on {}", bind_address); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .context("server error")?; + + Ok(()) +} + +async fn shutdown_signal() { + tokio::signal::ctrl_c() + .await + .expect("failed to install ctrl+c handler"); + tracing::info!("shutdown signal received"); +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs new file mode 100644 index 0000000..e0dff4f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs @@ -0,0 +1,29 @@ +// ©AngelaMos | 2026 +// cors.rs + +use axum::http::header::{HeaderName, ACCEPT, CONTENT_TYPE}; +use axum::http::Method; +use tower_http::cors::{Any, CorsLayer}; + +use crate::config::AppConfig; + +const ALLOWED_METHODS: [Method; 3] = + [Method::GET, Method::POST, Method::OPTIONS]; + +const ALLOWED_HEADERS: [HeaderName; 2] = + [CONTENT_TYPE, ACCEPT]; + +pub fn layer(config: &AppConfig) -> CorsLayer { + let base = CorsLayer::new() + .allow_methods(ALLOWED_METHODS) + .allow_headers(ALLOWED_HEADERS); + + if config.cors_origin == "*" { + base.allow_origin(Any) + } else { + base.allow_origin([config + .cors_origin + .parse() + .expect("invalid CORS origin header value")]) + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs new file mode 100644 index 0000000..9c449d6 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs @@ -0,0 +1,4 @@ +// ©AngelaMos | 2026 +// mod.rs + +pub mod cors; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs new file mode 100644 index 0000000..d524627 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs @@ -0,0 +1,66 @@ +// ©AngelaMos | 2026 +// analysis.rs + +use std::collections::HashMap; + +use axum::extract::{Path, State}; +use axum::Json; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +use crate::db::queries; +use crate::error::ApiError; +use crate::state::AppState; + +#[derive(Serialize)] +pub(crate) struct AnalysisResponse { + id: Uuid, + sha256: String, + file_name: String, + file_size: i64, + format: String, + architecture: String, + entry_point: Option, + threat_score: Option, + risk_level: Option, + slug: String, + created_at: DateTime, + passes: HashMap, +} + +pub async fn get_by_slug( + State(state): State, + Path(slug): Path, +) -> Result, ApiError> { + let row = queries::find_by_slug(&state.db, &slug) + .await? + .ok_or_else(|| ApiError::NotFound { + resource: format!("analysis '{slug}'"), + })?; + + let pass_rows = + queries::find_pass_results(&state.db, row.id) + .await?; + + let passes: HashMap = + pass_rows + .into_iter() + .map(|p| (p.pass_name, p.result)) + .collect(); + + Ok(Json(AnalysisResponse { + id: row.id, + sha256: row.sha256, + file_name: row.file_name, + file_size: row.file_size, + format: row.format, + architecture: row.architecture, + entry_point: row.entry_point, + threat_score: row.threat_score, + risk_level: row.risk_level, + slug: row.slug, + created_at: row.created_at, + passes, + })) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs new file mode 100644 index 0000000..7d25fc8 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs @@ -0,0 +1,30 @@ +// ©AngelaMos | 2026 +// health.rs + +use axum::extract::State; +use axum::Json; +use serde::Serialize; + +use crate::state::AppState; + +#[derive(Serialize)] +pub(crate) struct HealthResponse { + status: &'static str, + database: &'static str, +} + +pub async fn check( + State(state): State, +) -> Json { + let db_status = + match sqlx::query("SELECT 1").execute(&state.db).await + { + Ok(_) => "connected", + Err(_) => "disconnected", + }; + + Json(HealthResponse { + status: "ok", + database: db_status, + }) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs new file mode 100644 index 0000000..2ef0d4d --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs @@ -0,0 +1,21 @@ +// ©AngelaMos | 2026 +// mod.rs + +mod analysis; +mod health; +mod upload; + +use axum::routing::{get, post}; +use axum::Router; + +use crate::state::AppState; + +pub fn api_router() -> Router { + Router::new() + .route("/api/health", get(health::check)) + .route("/api/upload", post(upload::handle)) + .route( + "/api/analysis/{slug}", + get(analysis::get_by_slug), + ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs new file mode 100644 index 0000000..d1f8f63 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs @@ -0,0 +1,180 @@ +// ©AngelaMos | 2026 +// upload.rs + +use std::collections::HashMap; +use std::sync::Arc; + +use axum::extract::{Multipart, State}; +use axum::Json; +use serde::Serialize; +use uuid::Uuid; + +use axumortem_engine::context::AnalysisContext; +use axumortem_engine::pass::PassReport; + +use crate::db::models::{NewAnalysis, NewPassResult}; +use crate::db::queries; +use crate::error::ApiError; +use crate::state::AppState; + +const SLUG_LENGTH: usize = 12; + +const PASS_NAME_MAP: &[(&str, &str)] = &[ + ("disasm", "disassembly"), +]; + +#[derive(Serialize)] +pub(crate) struct UploadResponse { + slug: String, + cached: bool, +} + +pub async fn handle( + State(state): State, + mut multipart: Multipart, +) -> Result, ApiError> { + let (file_name, data) = + extract_file(&mut multipart).await?; + + let sha256 = + axumortem_engine::sha256_hex(&data); + + if let Some(slug) = + queries::find_slug_by_sha256( + &state.db, &sha256, + ) + .await? + { + return Ok(Json(UploadResponse { + slug, + cached: true, + })); + } + + let engine = Arc::clone(&state.engine); + let name_clone = file_name.clone(); + + let (ctx, report) = + tokio::task::spawn_blocking(move || { + engine.analyze(&data, &name_clone) + }) + .await?; + + let fmt = ctx.format_result.as_ref(); + let threat = ctx.threat_result.as_ref(); + let slug = sha256[..SLUG_LENGTH].to_string(); + + let new_analysis = NewAnalysis { + sha256, + file_name, + file_size: ctx.file_size as i64, + format: fmt + .map(|f| f.format.to_string()) + .unwrap_or_default(), + architecture: fmt + .map(|f| f.architecture.to_string()) + .unwrap_or_default(), + entry_point: fmt + .map(|f| f.entry_point as i64), + threat_score: threat + .map(|t| t.total_score as i32), + risk_level: threat + .map(|t| t.risk_level.to_string()), + slug: slug.clone(), + }; + + let mut tx = state.db.begin().await?; + + let row = + queries::insert_analysis(&mut tx, &new_analysis) + .await?; + + let pass_results = + build_pass_results(&ctx, &report, row.id)?; + for pr in &pass_results { + queries::insert_pass_result(&mut tx, pr).await?; + } + + tx.commit().await?; + + Ok(Json(UploadResponse { + slug, + cached: false, + })) +} + +async fn extract_file( + multipart: &mut Multipart, +) -> Result<(String, Vec), ApiError> { + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| ApiError::Internal { + reason: e.to_string(), + })? + { + if field.name() == Some("file") { + let name = field + .file_name() + .unwrap_or("unknown") + .to_string(); + let data = field + .bytes() + .await + .map_err(|e| ApiError::Internal { + reason: e.to_string(), + })?; + return Ok((name, data.to_vec())); + } + } + + Err(ApiError::NoFile) +} + +fn api_name(engine_name: &str) -> &str { + for &(from, to) in PASS_NAME_MAP { + if engine_name == from { + return to; + } + } + engine_name +} + +fn build_pass_results( + ctx: &AnalysisContext, + report: &PassReport, + analysis_id: Uuid, +) -> Result, serde_json::Error> { + let durations: HashMap<&str, u64> = report + .outcomes + .iter() + .map(|o| (o.name, o.duration_ms)) + .collect(); + + let mut results = Vec::new(); + + macro_rules! add_pass { + ($field:ident, $name:expr) => { + if let Some(ref r) = ctx.$field { + results.push(NewPassResult { + analysis_id, + pass_name: api_name($name) + .to_string(), + result: serde_json::to_value(r)?, + duration_ms: durations + .get($name) + .map(|&d| d as i32), + }); + } + }; + } + + add_pass!(format_result, "format"); + add_pass!(import_result, "imports"); + add_pass!(string_result, "strings"); + add_pass!(entropy_result, "entropy"); + add_pass!(disassembly_result, "disasm"); + add_pass!(threat_result, "threat"); + + Ok(results) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs new file mode 100644 index 0000000..936f3f7 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs @@ -0,0 +1,16 @@ +// ©AngelaMos | 2026 +// state.rs + +use std::sync::Arc; + +use axumortem_engine::AnalysisEngine; +use sqlx::PgPool; + +use crate::config::AppConfig; + +#[derive(Clone)] +pub struct AppState { + pub db: PgPool, + pub engine: Arc, + pub config: Arc, +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/cloudflared.compose.yml b/PROJECTS/intermediate/binary-analysis-tool/cloudflared.compose.yml new file mode 100644 index 0000000..ef876d9 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/cloudflared.compose.yml @@ -0,0 +1,27 @@ +# ============================================================================= +# AngelaMos | 2026 +# cloudflared.compose.yml +# ============================================================================= +# Cloudflare Tunnel for production remote access +# Usage: docker compose -f compose.yml -f cloudflared.compose.yml up -d +# ============================================================================= + +services: + cloudflared: + image: cloudflare/cloudflared:latest + container_name: ${APP_NAME:-axumortem}-tunnel + command: tunnel run --token ${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - app + depends_on: + nginx: + condition: service_started + deploy: + resources: + limits: + cpus: '0.5' + memory: 128M + reservations: + cpus: '0.1' + memory: 32M + restart: unless-stopped diff --git a/PROJECTS/intermediate/binary-analysis-tool/compose.yml b/PROJECTS/intermediate/binary-analysis-tool/compose.yml new file mode 100644 index 0000000..7928b85 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/compose.yml @@ -0,0 +1,92 @@ +# ©AngelaMos | 2026 +# compose.yml +# Production compose — Nginx serves frontend + proxies /api/* to backend +# For Cloudflare tunnel: docker compose -f compose.yml -f cloudflared.compose.yml up + +name: ${APP_NAME:-axumortem} + +services: + nginx: + build: + context: . + dockerfile: infra/docker/vite.prod + args: + - VITE_API_URL=${VITE_API_URL:-/api} + - VITE_APP_TITLE=${VITE_APP_TITLE:-axumortem} + container_name: ${APP_NAME:-axumortem}-nginx + ports: + - "${NGINX_HOST_PORT:-22784}:80" + depends_on: + backend: + condition: service_started + networks: + - app + deploy: + resources: + limits: + cpus: '1.0' + memory: 256M + reservations: + cpus: '0.25' + memory: 64M + restart: unless-stopped + + backend: + build: + context: . + dockerfile: infra/docker/rust.prod + container_name: ${APP_NAME:-axumortem}-backend + environment: + - DATABASE_URL=postgres://axumortem:${POSTGRES_PASSWORD:-axumortem}@postgres:5432/axumortem + - RUST_LOG=${RUST_LOG:-info} + - HOST=0.0.0.0 + - PORT=3000 + - MAX_UPLOAD_SIZE=${MAX_UPLOAD_SIZE:-52428800} + - CORS_ORIGIN=${CORS_ORIGIN:-*} + depends_on: + postgres: + condition: service_healthy + networks: + - app + deploy: + resources: + limits: + cpus: '2.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 256M + restart: unless-stopped + + postgres: + image: postgres:18-alpine + container_name: ${APP_NAME:-axumortem}-postgres + environment: + - POSTGRES_USER=axumortem + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-axumortem} + - POSTGRES_DB=axumortem + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U axumortem"] + interval: 10s + timeout: 3s + retries: 5 + networks: + - app + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + restart: unless-stopped + +networks: + app: + driver: bridge + +volumes: + pgdata: diff --git a/PROJECTS/intermediate/binary-analysis-tool/dev.compose.yml b/PROJECTS/intermediate/binary-analysis-tool/dev.compose.yml new file mode 100644 index 0000000..05eecbd --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/dev.compose.yml @@ -0,0 +1,95 @@ +# ©AngelaMos | 2026 +# dev.compose.yml +# Development compose — Nginx + Vite + Rust backend + PostgreSQL + +name: ${APP_NAME:-axumortem}-dev + +services: + nginx: + image: nginx:1.29-alpine + container_name: ${APP_NAME:-axumortem}-nginx-dev + ports: + - "${NGINX_HOST_PORT:-58495}:80" + volumes: + - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./infra/nginx/dev.nginx:/etc/nginx/conf.d/default.conf:ro + depends_on: + frontend: + condition: service_started + backend: + condition: service_started + networks: + - app + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: ../infra/docker/vite.dev + container_name: ${APP_NAME:-axumortem}-frontend-dev + ports: + - "${FRONTEND_HOST_PORT:-15723}:5173" + volumes: + - ./frontend:/app + - frontend_modules:/app/node_modules + environment: + - VITE_API_URL=${VITE_API_URL:-/api} + - VITE_APP_TITLE=${VITE_APP_TITLE:-axumortem} + networks: + - app + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: ../infra/docker/rust.dev + container_name: ${APP_NAME:-axumortem}-backend-dev + ports: + - "${BACKEND_HOST_PORT:-36968}:3000" + volumes: + - ./backend:/app + - cargo_registry:/usr/local/cargo/registry + - cargo_target:/app/target + environment: + - DATABASE_URL=postgres://axumortem:axumortem@postgres:5432/axumortem + - RUST_LOG=${RUST_LOG:-info,tower_http=debug} + - HOST=0.0.0.0 + - PORT=3000 + - MAX_UPLOAD_SIZE=${MAX_UPLOAD_SIZE:-52428800} + - CORS_ORIGIN=* + depends_on: + postgres: + condition: service_healthy + networks: + - app + restart: unless-stopped + + postgres: + image: postgres:18-alpine + container_name: ${APP_NAME:-axumortem}-postgres-dev + ports: + - "${POSTGRES_HOST_PORT:-5432}:5432" + environment: + - POSTGRES_USER=axumortem + - POSTGRES_PASSWORD=axumortem + - POSTGRES_DB=axumortem + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U axumortem"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - app + restart: unless-stopped + +networks: + app: + driver: bridge + +volumes: + frontend_modules: + cargo_registry: + cargo_target: + pgdata: diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/.dockerignore b/PROJECTS/intermediate/binary-analysis-tool/frontend/.dockerignore new file mode 100644 index 0000000..a0256ec --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +dist +.git +.gitignore +*.md +.env* +.vscode +.idea +*.log +npm-debug.log* +pnpm-debug.log* +.DS_Store +coverage +.nyc_output diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/.gitignore b/PROJECTS/intermediate/binary-analysis-tool/frontend/.gitignore new file mode 100644 index 0000000..61cb0c2 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/.stylelintignore b/PROJECTS/intermediate/binary-analysis-tool/frontend/.stylelintignore new file mode 100644 index 0000000..37da03e --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/.stylelintignore @@ -0,0 +1,22 @@ +# ©AngelaMos | 2025 +# .stylelintignore + +# Dependencies +node_modules/ + +# Production builds +dist/ +build/ +out/ + +# JS/TS files +**/*.js +**/*.jsx +**/*.ts +**/*.tsx + +# Generated files +*.min.css + +# Error system styles - ignore from linting +src/core/app/_toastStyles.scss diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/biome.json b/PROJECTS/intermediate/binary-analysis-tool/frontend/biome.json new file mode 100644 index 0000000..7f29029 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/biome.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 82, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 25 } + }, + "noForEach": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "noUndeclaredVariables": "error" + }, + "style": { + "useImportType": "error", + "useConst": "error", + "useTemplate": "error", + "useSelfClosingElements": "error", + "useFragmentSyntax": "error", + "noNonNullAssertion": "error", + "useConsistentArrayType": { + "level": "error", + "options": { "syntax": "shorthand" } + }, + "useNamingConvention": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noDebugger": "error", + "noConsole": "warn", + "noArrayIndexKey": "warn", + "noAssignInExpressions": "error", + "noDoubleEquals": "error", + "noRedeclare": "error", + "noVar": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "a11y": { + "useAltText": "error", + "useAnchorContent": "error", + "useKeyWithClickEvents": "error", + "noStaticElementInteractions": "error", + "useButtonType": "error", + "useValidAnchor": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/main.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/index.html b/PROJECTS/intermediate/binary-analysis-tool/frontend/index.html new file mode 100644 index 0000000..299f87a --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + Binary Analysis Tool + + + + +
+ + + diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/package.json b/PROJECTS/intermediate/binary-analysis-tool/frontend/package.json new file mode 100644 index 0000000..005327e --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/package.json @@ -0,0 +1,51 @@ +{ + "name": "binary-analysis-tool", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc -b", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix" + }, + "dependencies": { + "@dagrejs/dagre": "^3.0.0", + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.0", + "react": "^19.2.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-icon": "^1.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.1.1", + "sonner": "^2.0.7", + "zod": "^4.1.13", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/node": "^24.10.2", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.95.0", + "stylelint": "^16.26.1", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "vite": "npm:rolldown-vite@7.2.5", + "vite-tsconfig-paths": "^5.1.0" + }, + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/pnpm-lock.yaml b/PROJECTS/intermediate/binary-analysis-tool/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..b785d9f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/pnpm-lock.yaml @@ -0,0 +1,2636 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vite: npm:rolldown-vite@7.2.5 + +importers: + + .: + dependencies: + '@dagrejs/dagre': + specifier: ^3.0.0 + version: 3.0.0 + '@tanstack/react-query': + specifier: ^5.90.12 + version: 5.90.12(react@19.2.1) + axios: + specifier: ^1.13.0 + version: 1.13.2 + react: + specifier: ^19.2.1 + version: 19.2.1 + react-dom: + specifier: ^19.2.0 + version: 19.2.1(react@19.2.1) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.0(react@19.2.1) + react-icon: + specifier: ^1.0.0 + version: 1.0.0(babel-runtime@5.8.38)(react@19.2.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.1) + react-router-dom: + specifier: ^7.1.1 + version: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + zod: + specifier: ^4.1.13 + version: 4.1.13 + zustand: + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.2.7)(react@19.2.1) + devDependencies: + '@biomejs/biome': + specifier: ^2.3.8 + version: 2.3.8 + '@tanstack/react-query-devtools': + specifier: ^5.91.1 + version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1) + '@types/node': + specifier: ^24.10.2 + version: 24.10.2 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0)) + sass: + specifier: ^1.95.0 + version: 1.95.0 + stylelint: + specifier: ^16.26.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-prettier-scss: + specifier: ^1.0.0 + version: 1.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard-scss: + specifier: ^16.0.0 + version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: npm:rolldown-vite@7.2.5 + version: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + vite-tsconfig-paths: + specifier: ^5.1.0 + version: 5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.3.8': + resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.8': + resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.8': + resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.8': + resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.3.8': + resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.3.8': + resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.3.8': + resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.3.8': + resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.8': + resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.6': + resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==} + + '@cacheable/utils@2.3.2': + resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': + resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@dagrejs/dagre@3.0.0': + resolution: {integrity: sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==} + + '@dagrejs/graphlib@4.0.1': + resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.0': + resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.5.4 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/runtime@0.97.0': + resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.97.0': + resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.50': + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + + '@tanstack/query-devtools@5.91.1': + resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} + + '@tanstack/react-query-devtools@5.91.1': + resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.10 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/node@24.10.2': + resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + babel-runtime@5.8.38: + resolution: {integrity: sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + baseline-browser-mapping@2.9.5: + resolution: {integrity: sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==} + hasBin: true + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.3.0: + resolution: {integrity: sha512-HHiAvOBmlcR2f3SQ7kdlYD8+AUJG+wlFZ/Ze8tl1Vzvz0MdOh8IYA/EFU4ve8t1/sZ0j4MGi7ST5MoTwHessQA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@1.2.7: + resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.3.0: + resolution: {integrity: sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==} + engines: {node: '>=20'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookified@1.14.0: + resolution: {integrity: sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.5.5: + resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.25.0: + resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qified@0.5.3: + resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} + engines: {node: '>=20'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} + peerDependencies: + react: ^19.2.1 + + react-error-boundary@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + + react-icon@1.0.0: + resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==} + peerDependencies: + babel-runtime: ^5.3.3 + react: '>=0.12.0' + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.10.1: + resolution: {integrity: sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.10.1: + resolution: {integrity: sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-vite@7.2.5: + resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + esbuild: ^0.25.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + rolldown@1.0.0-beta.50: + resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.95.0: + resolution: {integrity: sha512-9QMjhLq+UkOg/4bb8Lt8A+hJZvY3t+9xeZMKSBtBEgxrXA3ed5Ts4NDreUkYgJP1BTmrscQE/xYhf7iShow6lw==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stylelint-config-prettier-scss@1.0.0: + resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==} + engines: {node: 14.* || 16.* || >= 18} + hasBin: true + peerDependencies: + stylelint: '>=15.0.0' + + stylelint-config-recommended-scss@16.0.2: + resolution: {integrity: sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.24.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard-scss@16.0.0: + resolution: {integrity: sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.23.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@39.0.1: + resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-scss@6.13.0: + resolution: {integrity: sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.8.2 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.3.8': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.8 + '@biomejs/cli-darwin-x64': 2.3.8 + '@biomejs/cli-linux-arm64': 2.3.8 + '@biomejs/cli-linux-arm64-musl': 2.3.8 + '@biomejs/cli-linux-x64': 2.3.8 + '@biomejs/cli-linux-x64-musl': 2.3.8 + '@biomejs/cli-win32-arm64': 2.3.8 + '@biomejs/cli-win32-x64': 2.3.8 + + '@biomejs/cli-darwin-arm64@2.3.8': + optional: true + + '@biomejs/cli-darwin-x64@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64@2.3.8': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-x64@2.3.8': + optional: true + + '@biomejs/cli-win32-arm64@2.3.8': + optional: true + + '@biomejs/cli-win32-x64@2.3.8': + optional: true + + '@cacheable/memory@2.0.6': + dependencies: + '@cacheable/utils': 2.3.2 + '@keyv/bigmap': 1.3.0(keyv@5.5.5) + hookified: 1.14.0 + keyv: 5.5.5 + + '@cacheable/utils@2.3.2': + dependencies: + hashery: 1.3.0 + keyv: 5.5.5 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.20': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@dagrejs/dagre@3.0.0': + dependencies: + '@dagrejs/graphlib': 4.0.1 + + '@dagrejs/graphlib@4.0.1': {} + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.0(keyv@5.5.5)': + dependencies: + hashery: 1.3.0 + hookified: 1.14.0 + keyv: 5.5.5 + + '@keyv/serialize@1.1.1': {} + + '@napi-rs/wasm-runtime@1.1.0': + dependencies: + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oxc-project/runtime@0.97.0': {} + + '@oxc-project/types@0.97.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + dependencies: + '@napi-rs/wasm-runtime': 1.1.0 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.50': {} + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@tanstack/query-core@5.90.12': {} + + '@tanstack/query-devtools@5.91.1': {} + + '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)': + dependencies: + '@tanstack/query-devtools': 5.91.1 + '@tanstack/react-query': 5.90.12(react@19.2.1) + react: 19.2.1 + + '@tanstack/react-query@5.90.12(react@19.2.1)': + dependencies: + '@tanstack/query-core': 5.90.12 + react: 19.2.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/node@24.10.2': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-runtime@5.8.38: + dependencies: + core-js: 1.2.7 + + balanced-match@2.0.0: {} + + baseline-browser-mapping@2.9.5: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.5 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) + + cacheable@2.3.0: + dependencies: + '@cacheable/memory': 2.0.6 + '@cacheable/utils': 2.3.2 + hookified: 1.14.0 + keyv: 5.5.5 + qified: 0.5.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001759: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + core-js@1.2.7: {} + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@11.1.1: + dependencies: + flat-cache: 6.1.19 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.19: + dependencies: + cacheable: 2.3.0 + flatted: 3.3.3 + hookified: 1.14.0 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.3.0: + dependencies: + hookified: 1.14.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookified@1.14.0: {} + + html-tags@3.3.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + keyv@5.5.5: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.37.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.25.0: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + qified@0.5.3: + dependencies: + hookified: 1.14.0 + + queue-microtask@1.2.3: {} + + react-dom@19.2.1(react@19.2.1): + dependencies: + react: 19.2.1 + scheduler: 0.27.0 + + react-error-boundary@6.0.0(react@19.2.1): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.1 + + react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.1): + dependencies: + babel-runtime: 5.8.38 + react: 19.2.1 + + react-icons@5.5.0(react@19.2.1): + dependencies: + react: 19.2.1 + + react-refresh@0.18.0: {} + + react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-router: 7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + + react-router@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + cookie: 1.1.1 + react: 19.2.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.1(react@19.2.1) + + react@19.2.1: {} + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0): + dependencies: + '@oxc-project/runtime': 0.97.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.50 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.2 + fsevents: 2.3.3 + sass: 1.95.0 + + rolldown@1.0.0-beta.50: + dependencies: + '@oxc-project/types': 0.97.0 + '@rolldown/pluginutils': 1.0.0-beta.50 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-x64': 1.0.0-beta.50 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.50 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.95.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sonner@2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-scss: 6.13.0(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + + stylelint-scss@6.13.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.25.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 16.26.1(typescript@5.9.3) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.0.20 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.2(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0))(typescript@5.9.3): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0) + transitivePeerDependencies: + - supports-color + - typescript + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + yallist@3.1.1: {} + + zod@4.1.13: {} + + zustand@5.0.9(@types/react@19.2.7)(react@19.2.1): + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.1 diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000..7788e65 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-192x192.png differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-512x512.png b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-512x512.png new file mode 100644 index 0000000..eb1b843 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/android-chrome-512x512.png differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/apple-touch-icon.png b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..e8f1762 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/apple-touch-icon.png differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-16x16.png b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-16x16.png new file mode 100644 index 0000000..7062495 Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-16x16.png differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-32x32.png b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-32x32.png new file mode 100644 index 0000000..48cd1bc Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon-32x32.png differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon.ico b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon.ico new file mode 100644 index 0000000..bf4991d Binary files /dev/null and b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/favicon.ico differ diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/site.webmanifest b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/site.webmanifest new file mode 100644 index 0000000..1dd9112 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/public/assets/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx new file mode 100644 index 0000000..a7d82d6 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx @@ -0,0 +1,36 @@ +// =========================== +// ©AngelaMos | 2026 +// App.tsx +// =========================== + +import { QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { RouterProvider } from 'react-router-dom' +import { Toaster } from 'sonner' + +import { queryClient } from '@/core/api' +import { router } from '@/core/app/routers' +import '@/core/app/toast.module.scss' + +export default function App(): React.ReactElement { + return ( + +
+ + +
+ +
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts new file mode 100644 index 0000000..383e6e6 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts @@ -0,0 +1,48 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import { useMutation, useQuery } from '@tanstack/react-query' +import type { AxiosError } from 'axios' +import { API_ENDPOINTS, QUERY_KEYS, UPLOAD_TIMEOUT_MS } from '@/config' +import { apiClient } from '@/core/api' +import { transformAxiosError } from '@/core/api/errors' +import { AnalysisResponseSchema, UploadResponseSchema } from '../schemas' +import type { ApiErrorBody, UploadResponse } from '../types' + +export function useUpload() { + return useMutation< + UploadResponse, + ReturnType, + File + >({ + mutationFn: async (file: File) => { + const form = new FormData() + form.append('file', file) + + const { data } = await apiClient.post(API_ENDPOINTS.UPLOAD, form, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: UPLOAD_TIMEOUT_MS, + }) + + return UploadResponseSchema.parse(data) + }, + onError: (error) => { + return transformAxiosError(error as unknown as AxiosError) + }, + }) +} + +export function useAnalysis(slug: string) { + return useQuery({ + queryKey: QUERY_KEYS.ANALYSIS.BY_SLUG(slug), + queryFn: async () => { + const { data } = await apiClient.get(API_ENDPOINTS.ANALYSIS(slug)) + return AnalysisResponseSchema.parse(data) + }, + enabled: slug.length > 0, + staleTime: Infinity, + refetchOnWindowFocus: false, + }) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts new file mode 100644 index 0000000..8c2d64e --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './hooks' +export * from './schemas' +export * from './types' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts new file mode 100644 index 0000000..ad691b0 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts @@ -0,0 +1,397 @@ +// =================== +// © AngelaMos | 2026 +// schemas.ts +// =================== + +import { z } from 'zod' + +export const BinaryFormatSchema = z.enum(['Elf', 'Pe', 'MachO']) + +export const ArchitectureSchema = z.union([ + z.literal(['X86', 'X86_64', 'Arm', 'Aarch64']), + z.object({ Other: z.string() }), +]) + +export const EndiannessSchema = z.enum(['Little', 'Big']) + +export const RiskLevelSchema = z.enum([ + 'Benign', + 'Low', + 'Medium', + 'High', + 'Critical', +]) + +export const SeveritySchema = z.enum(['Low', 'Medium', 'High', 'Critical']) + +export const StringEncodingSchema = z.enum(['Ascii', 'Utf8', 'Utf16Le']) + +export const StringCategorySchema = z.enum([ + 'Url', + 'IpAddress', + 'FilePath', + 'RegistryKey', + 'ShellCommand', + 'CryptoWallet', + 'Email', + 'SuspiciousApi', + 'PackerSignature', + 'DebugArtifact', + 'AntiAnalysis', + 'PersistencePath', + 'EncodedData', + 'Generic', +]) + +export const EntropyClassificationSchema = z.enum([ + 'Plaintext', + 'NativeCode', + 'Compressed', + 'Packed', + 'Encrypted', +]) + +export const EntropyFlagSchema = z.enum([ + 'HighEntropy', + 'HighVirtualToRawRatio', + 'EmptyRawData', + 'Rwx', + 'PackerSectionName', +]) + +export const FlowControlTypeSchema = z.enum([ + 'Next', + 'Branch', + 'ConditionalBranch', + 'Call', + 'Return', + 'Interrupt', +]) + +export const CfgEdgeTypeSchema = z.enum([ + 'Fallthrough', + 'ConditionalTrue', + 'ConditionalFalse', + 'Unconditional', + 'Call', +]) + +export const SectionPermissionsSchema = z.object({ + read: z.boolean(), + write: z.boolean(), + execute: z.boolean(), +}) + +export const SectionInfoSchema = z.object({ + name: z.string(), + virtual_address: z.number(), + virtual_size: z.number(), + raw_offset: z.number(), + raw_size: z.number(), + permissions: SectionPermissionsSchema, + sha256: z.string(), +}) + +export const SegmentInfoSchema = z.object({ + name: z.string().nullable(), + virtual_address: z.number(), + virtual_size: z.number(), + file_offset: z.number(), + file_size: z.number(), + permissions: SectionPermissionsSchema, +}) + +export const FormatAnomalySchema = z.union([ + z.string(), + z.record(z.string(), z.unknown()), +]) + +export const PeDllCharacteristicsSchema = z.object({ + aslr: z.boolean(), + dep: z.boolean(), + cfg: z.boolean(), + no_seh: z.boolean(), + force_integrity: z.boolean(), +}) + +export const PeInfoSchema = z.object({ + image_base: z.number(), + subsystem: z.string(), + dll_characteristics: PeDllCharacteristicsSchema, + timestamp: z.number(), + linker_version: z.string(), + tls_callback_count: z.number(), + has_overlay: z.boolean(), + overlay_size: z.number(), + rich_header_present: z.boolean(), +}) + +export const ElfInfoSchema = z.object({ + os_abi: z.string(), + elf_type: z.string(), + interpreter: z.string().nullable(), + gnu_relro: z.boolean(), + bind_now: z.boolean(), + stack_executable: z.boolean(), + needed_libraries: z.array(z.string()), +}) + +export const MachOInfoSchema = z.object({ + file_type: z.string(), + cpu_subtype: z.string(), + is_universal: z.boolean(), + has_code_signature: z.boolean(), + min_os_version: z.string().nullable(), + sdk_version: z.string().nullable(), + dylibs: z.array(z.string()), + has_function_starts: z.boolean(), +}) + +export const FormatResultSchema = z.object({ + format: BinaryFormatSchema, + architecture: ArchitectureSchema, + bits: z.number(), + endianness: EndiannessSchema, + entry_point: z.number(), + is_stripped: z.boolean(), + is_pie: z.boolean(), + has_debug_info: z.boolean(), + sections: z.array(SectionInfoSchema), + segments: z.array(SegmentInfoSchema), + anomalies: z.array(FormatAnomalySchema), + pe_info: PeInfoSchema.nullable(), + elf_info: ElfInfoSchema.nullable(), + macho_info: MachOInfoSchema.nullable(), + function_hints: z.array(z.number()).default([]), +}) + +export const ImportEntrySchema = z.object({ + library: z.string(), + function: z.string(), + address: z.number().nullable(), + ordinal: z.number().nullable(), + is_suspicious: z.boolean(), + threat_tags: z.array(z.string()), +}) + +export const ExportEntrySchema = z.object({ + name: z.string().nullable(), + address: z.number(), + ordinal: z.number().nullable(), + is_forwarded: z.boolean(), + forward_target: z.string().nullable(), +}) + +export const SuspiciousCombinationSchema = z.object({ + name: z.string(), + description: z.string(), + apis: z.array(z.string()), + mitre_id: z.string(), + severity: SeveritySchema, +}) + +export const ImportMitreMappingSchema = z.object({ + technique_id: z.string(), + api: z.string(), + tag: z.string(), +}) + +export const ImportStatisticsSchema = z.object({ + total_imports: z.number(), + total_exports: z.number(), + suspicious_count: z.number(), + library_count: z.number(), +}) + +export const ImportResultSchema = z.object({ + imports: z.array(ImportEntrySchema), + exports: z.array(ExportEntrySchema), + libraries: z.array(z.string()), + suspicious_combinations: z.array(SuspiciousCombinationSchema), + mitre_mappings: z.array(ImportMitreMappingSchema), + statistics: ImportStatisticsSchema, +}) + +export const ExtractedStringSchema = z.object({ + value: z.string(), + offset: z.number(), + encoding: StringEncodingSchema, + length: z.number(), + category: StringCategorySchema, + is_suspicious: z.boolean(), + section: z.string().nullable(), +}) + +export const StringStatisticsSchema = z.object({ + total: z.number(), + by_encoding: z.record(z.string(), z.number()), + by_category: z.record(z.string(), z.number()), + suspicious_count: z.number(), +}) + +export const StringResultSchema = z.object({ + strings: z.array(ExtractedStringSchema), + statistics: StringStatisticsSchema, +}) + +export const SectionEntropySchema = z.object({ + name: z.string(), + entropy: z.number(), + size: z.number(), + classification: EntropyClassificationSchema, + virtual_to_raw_ratio: z.number(), + is_anomalous: z.boolean(), + flags: z.array(EntropyFlagSchema), +}) + +export const PackingIndicatorSchema = z.object({ + indicator_type: z.string(), + description: z.string(), + evidence: z.string(), + packer_name: z.string().nullable(), +}) + +export const EntropyResultSchema = z.object({ + overall_entropy: z.number(), + sections: z.array(SectionEntropySchema), + packing_detected: z.boolean(), + packer_name: z.string().nullable(), + packing_indicators: z.array(PackingIndicatorSchema), +}) + +export const InstructionInfoSchema = z.object({ + address: z.number(), + bytes: z.array(z.number()), + mnemonic: z.string(), + operands: z.string(), + size: z.number(), + flow_control: FlowControlTypeSchema, +}) + +export const BasicBlockInfoSchema = z.object({ + start_address: z.number(), + end_address: z.number(), + instruction_count: z.number(), + instructions: z.array(InstructionInfoSchema), + successors: z.array(z.number()), + predecessors: z.array(z.number()), +}) + +export const CfgNodeSchema = z.object({ + id: z.number(), + label: z.string(), + instruction_count: z.number(), + instructions_preview: z.string(), +}) + +export const CfgEdgeSchema = z.object({ + from: z.number(), + to: z.number(), + edge_type: CfgEdgeTypeSchema, +}) + +export const FunctionCfgSchema = z.object({ + nodes: z.array(CfgNodeSchema), + edges: z.array(CfgEdgeSchema), +}) + +export const FunctionInfoSchema = z.object({ + address: z.number(), + name: z.string().nullable(), + size: z.number(), + instruction_count: z.number(), + basic_blocks: z.array(BasicBlockInfoSchema), + is_entry_point: z.boolean(), + cfg: FunctionCfgSchema, +}) + +export const DisassemblyResultSchema = z.object({ + functions: z.array(FunctionInfoSchema), + total_instructions: z.number(), + total_functions: z.number(), + architecture_bits: z.number(), + entry_function_address: z.number(), +}) + +export const ScoringDetailSchema = z.object({ + rule: z.string(), + points: z.number(), + evidence: z.string(), +}) + +export const ScoringCategorySchema = z.object({ + name: z.string(), + score: z.number(), + max_score: z.number(), + details: z.array(ScoringDetailSchema), +}) + +export const ThreatMitreMappingSchema = z.object({ + technique_id: z.string(), + technique_name: z.string(), + tactic: z.string(), + evidence: z.string(), +}) + +export const YaraMetadataSchema = z.object({ + description: z.string().nullable(), + category: z.string().nullable(), + severity: z.string().nullable(), +}) + +export const YaraStringMatchSchema = z.object({ + identifier: z.string(), + match_count: z.number(), +}) + +export const YaraMatchSchema = z.object({ + rule_name: z.string(), + tags: z.array(z.string()), + metadata: YaraMetadataSchema, + matched_strings: z.array(YaraStringMatchSchema), +}) + +export const ThreatResultSchema = z.object({ + total_score: z.number(), + risk_level: RiskLevelSchema, + categories: z.array(ScoringCategorySchema), + mitre_techniques: z.array(ThreatMitreMappingSchema), + yara_matches: z.array(YaraMatchSchema), + summary: z.string(), +}) + +export const AnalysisPassesSchema = z.object({ + format: FormatResultSchema.optional(), + imports: ImportResultSchema.optional(), + strings: StringResultSchema.optional(), + entropy: EntropyResultSchema.optional(), + disassembly: DisassemblyResultSchema.optional(), + threat: ThreatResultSchema.optional(), +}) + +export const AnalysisResponseSchema = z.object({ + id: z.string(), + sha256: z.string(), + file_name: z.string(), + file_size: z.number(), + format: z.string(), + architecture: z.string(), + entry_point: z.number().nullable(), + threat_score: z.number().nullable(), + risk_level: z.string().nullable(), + slug: z.string(), + created_at: z.string(), + passes: AnalysisPassesSchema, +}) + +export const UploadResponseSchema = z.object({ + slug: z.string(), + cached: z.boolean(), +}) + +export const ApiErrorBodySchema = z.object({ + error: z.object({ + code: z.string(), + message: z.string(), + }), +}) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts new file mode 100644 index 0000000..37c7bd7 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts @@ -0,0 +1,116 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +import type { z } from 'zod' +import type { + AnalysisPassesSchema, + AnalysisResponseSchema, + ApiErrorBodySchema, + ArchitectureSchema, + BasicBlockInfoSchema, + BinaryFormatSchema, + CfgEdgeSchema, + CfgEdgeTypeSchema, + CfgNodeSchema, + DisassemblyResultSchema, + ElfInfoSchema, + EndiannessSchema, + EntropyClassificationSchema, + EntropyFlagSchema, + EntropyResultSchema, + ExportEntrySchema, + ExtractedStringSchema, + FlowControlTypeSchema, + FormatAnomalySchema, + FormatResultSchema, + FunctionCfgSchema, + FunctionInfoSchema, + ImportEntrySchema, + ImportMitreMappingSchema, + ImportResultSchema, + ImportStatisticsSchema, + InstructionInfoSchema, + MachOInfoSchema, + PackingIndicatorSchema, + PeDllCharacteristicsSchema, + PeInfoSchema, + RiskLevelSchema, + ScoringCategorySchema, + ScoringDetailSchema, + SectionEntropySchema, + SectionInfoSchema, + SectionPermissionsSchema, + SegmentInfoSchema, + SeveritySchema, + StringCategorySchema, + StringEncodingSchema, + StringResultSchema, + StringStatisticsSchema, + SuspiciousCombinationSchema, + ThreatMitreMappingSchema, + ThreatResultSchema, + UploadResponseSchema, + YaraMatchSchema, + YaraMetadataSchema, + YaraStringMatchSchema, +} from '../schemas' + +export type BinaryFormat = z.infer +export type Architecture = z.infer +export type Endianness = z.infer +export type RiskLevel = z.infer +export type Severity = z.infer +export type StringEncoding = z.infer +export type StringCategory = z.infer +export type EntropyClassification = z.infer +export type EntropyFlag = z.infer +export type FlowControlType = z.infer +export type CfgEdgeType = z.infer + +export type SectionPermissions = z.infer +export type SectionInfo = z.infer +export type SegmentInfo = z.infer +export type FormatAnomaly = z.infer +export type PeDllCharacteristics = z.infer +export type PeInfo = z.infer +export type ElfInfo = z.infer +export type MachOInfo = z.infer +export type FormatResult = z.infer + +export type ImportEntry = z.infer +export type ExportEntry = z.infer +export type SuspiciousCombination = z.infer +export type ImportMitreMapping = z.infer +export type ImportStatistics = z.infer +export type ImportResult = z.infer + +export type ExtractedString = z.infer +export type StringStatistics = z.infer +export type StringResult = z.infer + +export type SectionEntropy = z.infer +export type PackingIndicator = z.infer +export type EntropyResult = z.infer + +export type InstructionInfo = z.infer +export type BasicBlockInfo = z.infer +export type CfgNode = z.infer +export type CfgEdge = z.infer +export type FunctionCfg = z.infer +export type FunctionInfo = z.infer +export type DisassemblyResult = z.infer + +export type ScoringDetail = z.infer +export type ScoringCategory = z.infer +export type ThreatMitreMapping = z.infer +export type YaraMetadata = z.infer +export type YaraStringMatch = z.infer +export type YaraMatch = z.infer +export type ThreatResult = z.infer + +export type AnalysisPasses = z.infer +export type AnalysisResponse = z.infer +export type UploadResponse = z.infer +export type ApiErrorBody = z.infer diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts new file mode 100644 index 0000000..58faf70 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts @@ -0,0 +1,76 @@ +// =================== +// © AngelaMos | 2026 +// config.ts +// =================== + +export const API_ENDPOINTS = { + UPLOAD: '/upload', + ANALYSIS: (slug: string) => `/analysis/${slug}`, + HEALTH: '/health', +} as const + +export const QUERY_KEYS = { + ANALYSIS: { + BY_SLUG: (slug: string) => ['analysis', slug] as const, + }, +} as const + +export const ROUTES = { + HOME: '/', + ANALYSIS: '/analysis/:slug', +} as const + +export const STORAGE_KEYS = { + UI: 'ui-storage', +} as const + +export const QUERY_CONFIG = { + STALE_TIME: { + USER: 1000 * 60 * 5, + STATIC: Infinity, + FREQUENT: 1000 * 30, + }, + GC_TIME: { + DEFAULT: 1000 * 60 * 30, + LONG: 1000 * 60 * 60, + }, + RETRY: { + DEFAULT: 3, + NONE: 0, + }, +} as const + +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + TOO_MANY_REQUESTS: 429, + INTERNAL_SERVER: 500, +} as const + +export const UPLOAD_TIMEOUT_MS = 120_000 + +export const RISK_LEVEL_COLORS: Record = { + Benign: '#22c55e', + Low: '#84cc16', + Medium: '#eab308', + High: '#f97316', + Critical: '#ef4444', +} as const + +export const ENTROPY_CLASSIFICATION_COLORS: Record = { + Plaintext: '#22c55e', + NativeCode: '#3b82f6', + Compressed: '#eab308', + Packed: '#f97316', + Encrypted: '#ef4444', +} as const + +export type ApiEndpoint = typeof API_ENDPOINTS +export type QueryKey = typeof QUERY_KEYS +export type Route = typeof ROUTES diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts new file mode 100644 index 0000000..f48a4d0 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts @@ -0,0 +1,17 @@ +// =================== +// © AngelaMos | 2026 +// api.config.ts +// =================== + +import axios, { type AxiosInstance } from 'axios' + +const getBaseURL = (): string => { + return import.meta.env.VITE_API_URL ?? '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, +}) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts new file mode 100644 index 0000000..492672f --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts @@ -0,0 +1,114 @@ +/** + * ©AngelaMos | 2026 + * errors.ts + */ + +import type { AxiosError } from 'axios' + +export const ApiErrorCode = { + NETWORK_ERROR: 'NETWORK_ERROR', + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + CONFLICT: 'CONFLICT', + RATE_LIMITED: 'RATE_LIMITED', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', +} as const + +export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode] + +export class ApiError extends Error { + readonly code: ApiErrorCode + readonly statusCode: number + readonly details?: Record + + constructor( + message: string, + code: ApiErrorCode, + statusCode: number, + details?: Record + ) { + super(message) + this.name = 'ApiError' + this.code = code + this.statusCode = statusCode + this.details = details + } + + getUserMessage(): string { + const messages: Record = { + [ApiErrorCode.NETWORK_ERROR]: + 'Unable to connect. Please check your internet connection.', + [ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.', + [ApiErrorCode.AUTHENTICATION_ERROR]: + 'Your session has expired. Please log in again.', + [ApiErrorCode.AUTHORIZATION_ERROR]: + 'You do not have permission to perform this action.', + [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.', + [ApiErrorCode.CONFLICT]: + 'This operation conflicts with an existing resource.', + [ApiErrorCode.RATE_LIMITED]: + 'Too many requests. Please wait a moment and try again.', + [ApiErrorCode.SERVER_ERROR]: + 'Something went wrong on our end. Please try again later.', + [ApiErrorCode.UNKNOWN_ERROR]: + 'An unexpected error occurred. Please try again.', + } + return messages[this.code] + } +} + +interface ApiErrorResponse { + detail?: string | { msg: string; type: string }[] + message?: string +} + +export function transformAxiosError(error: AxiosError): ApiError { + if (!error.response) { + return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0) + } + + const { status } = error.response + const data = error.response.data as ApiErrorResponse | undefined + let message = 'An error occurred' + let details: Record | undefined + + if (data?.detail) { + if (typeof data.detail === 'string') { + message = data.detail + } else if (Array.isArray(data.detail)) { + details = { validation: [] } + data.detail.forEach((err) => { + details?.validation.push(err.msg) + }) + message = 'Validation error' + } + } else if (data?.message) { + message = data.message + } + + const codeMap: Record = { + 400: ApiErrorCode.VALIDATION_ERROR, + 401: ApiErrorCode.AUTHENTICATION_ERROR, + 403: ApiErrorCode.AUTHORIZATION_ERROR, + 404: ApiErrorCode.NOT_FOUND, + 409: ApiErrorCode.CONFLICT, + 429: ApiErrorCode.RATE_LIMITED, + 500: ApiErrorCode.SERVER_ERROR, + 502: ApiErrorCode.SERVER_ERROR, + 503: ApiErrorCode.SERVER_ERROR, + 504: ApiErrorCode.SERVER_ERROR, + } + + const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR + + return new ApiError(message, code, status, details) +} + +declare module '@tanstack/react-query' { + interface Register { + defaultError: ApiError + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts new file mode 100644 index 0000000..f6dc363 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './api.config' +export * from './errors' +export * from './query.config' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts new file mode 100644 index 0000000..42084ec --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts @@ -0,0 +1,105 @@ +// =================== +// © AngelaMos | 2026 +// query.config.ts +// =================== + +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { QUERY_CONFIG } from '@/config' +import { ApiError, ApiErrorCode } from './errors' + +const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [ + ApiErrorCode.AUTHENTICATION_ERROR, + ApiErrorCode.AUTHORIZATION_ERROR, + ApiErrorCode.NOT_FOUND, + ApiErrorCode.VALIDATION_ERROR, +] as const + +const shouldRetryQuery = (failureCount: number, error: Error): boolean => { + if (error instanceof ApiError) { + if (NO_RETRY_ERROR_CODES.includes(error.code)) { + return false + } + } + return failureCount < QUERY_CONFIG.RETRY.DEFAULT +} + +const calculateRetryDelay = (attemptIndex: number): number => { + const baseDelay = 1000 + const maxDelay = 30000 + return Math.min(baseDelay * 2 ** attemptIndex, maxDelay) +} + +const handleQueryCacheError = ( + error: Error, + query: { state: { data: unknown } } +): void => { + if (query.state.data !== undefined) { + const message = + error instanceof ApiError + ? error.getUserMessage() + : 'Background update failed' + toast.error(message) + } +} + +const handleMutationCacheError = ( + error: Error, + _variables: unknown, + _context: unknown, + mutation: { options: { onError?: unknown } } +): void => { + if (mutation.options.onError === undefined) { + const message = + error instanceof ApiError ? error.getUserMessage() : 'Operation failed' + toast.error(message) + } +} + +export const QUERY_STRATEGIES = { + standard: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + }, + frequent: { + staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT, + }, + static: { + staleTime: QUERY_CONFIG.STALE_TIME.STATIC, + gcTime: QUERY_CONFIG.GC_TIME.LONG, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + auth: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: QUERY_CONFIG.RETRY.NONE, + }, +} as const + +export type QueryStrategy = keyof typeof QUERY_STRATEGIES + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: shouldRetryQuery, + retryDelay: calculateRetryDelay, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true, + }, + mutations: { + retry: QUERY_CONFIG.RETRY.NONE, + }, + }, + queryCache: new QueryCache({ + onError: handleQueryCacheError, + }), + mutationCache: new MutationCache({ + onError: handleMutationCacheError, + }), +}) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx new file mode 100644 index 0000000..8958396 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx @@ -0,0 +1,30 @@ +// =================== +// © AngelaMos | 2026 +// routers.tsx +// =================== + +import { createBrowserRouter, type RouteObject } from 'react-router-dom' +import { ROUTES } from '@/config' +import { Shell } from './shell' + +const routes: RouteObject[] = [ + { + element: , + children: [ + { + path: ROUTES.HOME, + lazy: () => import('@/pages/landing'), + }, + { + path: ROUTES.ANALYSIS, + lazy: () => import('@/pages/analysis'), + }, + { + path: '*', + lazy: () => import('@/pages/landing'), + }, + ], + }, +] + +export const router = createBrowserRouter(routes) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.module.scss b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.module.scss new file mode 100644 index 0000000..53789f3 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.module.scss @@ -0,0 +1,331 @@ +// =================== +// © AngelaMos | 2026 +// shell.module.scss +// =================== + +@use '@/styles' as *; + +$sidebar-width: 240px; +$sidebar-collapsed-width: 64px; +$header-height: 56px; + +.shell { + display: flex; + min-height: 100vh; + min-height: 100dvh; +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: $sidebar-width; + background: $bg-surface-100; + border-right: 1px solid $border-default; + display: flex; + flex-direction: column; + z-index: $z-fixed; + @include transition-fast; + + &.collapsed { + width: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + transform: translateX(-100%); + + &.open { + transform: translateX(0); + } + + &.collapsed { + width: $sidebar-width; + } + } +} + +.sidebarHeader { + height: $header-height; + padding: 0 $space-3; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $border-default; + + .sidebar.collapsed & { + justify-content: center; + padding: 0; + } +} + +.logo { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.nav { + flex: 1; + padding: $space-3; + display: flex; + flex-direction: column; + gap: $space-1; +} + +.navItem { + display: flex; + align-items: center; + gap: $space-3; + padding: $space-2 $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-light; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &.active { + background: $bg-selection; + color: $text-default; + } + + .sidebar.collapsed & { + justify-content: center; + } +} + +.navIcon { + width: 17px; + height: 17px; + flex-shrink: 0; +} + +.navLabel { + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.adminItem { + margin-top: auto; + border-top: 1px solid $border-default; + padding-top: $space-3; +} + +.collapseBtn { + width: 45px; + height: 45px; + border-radius: $radius-md; + color: $text-light; + @include flex-center; + @include transition-fast; + + svg { + width: 23.5px; + height: 23.5px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @include breakpoint-down('sm') { + display: none; + } +} + +.sidebarFooter { + padding: $space-3; + border-top: 1px solid $border-default; +} + +.logoutBtn { + width: 100%; + display: flex; + align-items: center; + gap: $space-3; + padding: $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + } + + .sidebar.collapsed & { + justify-content: center; + + .logoutText { + display: none; + } + } +} + +.logoutIcon { + width: 18px; + height: 18px; + flex-shrink: 0; +} + +.logoutText { + font-weight: $font-weight-medium; + @include transition-fast; +} + +.overlay { + position: fixed; + inset: 0; + background: rgb(0, 0, 0, 50%); + z-index: calc($z-fixed - 1); + display: none; + border: none; + padding: 0; + cursor: pointer; + + @include breakpoint-down('sm') { + display: block; + } +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + margin-left: $sidebar-width; + min-width: 0; + @include transition-fast; + + &.collapsed { + margin-left: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + margin-left: 0; + + &.collapsed { + margin-left: 0; + } + } +} + +.header { + position: sticky; + top: 0; + height: $header-height; + background: $bg-surface-100; + border-bottom: 1px solid $border-default; + z-index: $z-sticky; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 $space-4; +} + +.headerLeft { + display: flex; + align-items: center; + gap: $space-3; +} + +.menuBtn { + display: none; + width: 36px; + height: 36px; + border-radius: $radius-md; + color: $text-light; + align-items: center; + justify-content: center; + @include transition-fast; + + svg { + width: 20px; + height: 20px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @media (width <= 479px) { + display: flex; + } +} + +.pageTitle { + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + margin-left: 7px; +} + +.headerRight { + display: flex; + align-items: center; + gap: $space-3; +} + +.avatar { + width: 32px; + height: 32px; + border-radius: $radius-full; + background: $bg-surface-300; + color: $text-light; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + @include flex-center; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(1.2); + } +} + +.content { + flex: 1; + overflow-y: auto; +} + +.loading { + @include flex-center; + height: 100%; + color: $text-muted; +} + +.error { + @include flex-column-center; + height: 100%; + gap: $space-4; + padding: $space-6; + color: $error-default; + + h2 { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + } + + pre { + font-family: $font-mono; + font-size: $font-size-sm; + padding: $space-4; + background: $bg-surface-200; + border-radius: $radius-lg; + overflow-x: auto; + max-width: 100%; + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx new file mode 100644 index 0000000..6bfd851 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx @@ -0,0 +1,36 @@ +// =================== +// © AngelaMos | 2026 +// shell.tsx +// =================== + +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { Outlet } from 'react-router-dom' +import styles from './shell.module.scss' + +function ShellErrorFallback({ error }: { error: Error }): React.ReactElement { + return ( +
+

Something went wrong

+
{error.message}
+
+ ) +} + +function ShellLoading(): React.ReactElement { + return
Loading...
+} + +export function Shell(): React.ReactElement { + return ( +
+
+ + }> + + + +
+
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/toast.module.scss b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/toast.module.scss new file mode 100644 index 0000000..29b3f61 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/toast.module.scss @@ -0,0 +1,67 @@ +// =================== +// © AngelaMos | 2026 +// toast.module.scss +// =================== + +@use '@/styles' as *; + +:global { + [data-sonner-toaster] { + --normal-bg: #{$bg-surface-100}; + --normal-border: #{$border-default}; + --normal-text: #{$text-default}; + + --success-bg: #{$bg-surface-100}; + --success-border: #{$border-default}; + --success-text: #{$text-default}; + + --error-bg: #{$bg-surface-100}; + --error-border: #{$error-default}; + --error-text: #{$text-default}; + + --warning-bg: #{$bg-surface-100}; + --warning-border: #{$border-default}; + --warning-text: #{$text-default}; + + --info-bg: #{$bg-surface-100}; + --info-border: #{$border-default}; + --info-text: #{$text-default}; + + font-family: $font-sans; + } + + [data-sonner-toast] { + border-radius: $radius-md; + padding: $space-3 $space-4; + font-size: $font-size-sm; + border: 1px solid $border-default; + background: $bg-surface-100; + color: $text-default; + + [data-title] { + font-weight: $font-weight-medium; + } + + [data-description] { + color: $text-light; + font-size: $font-size-xs; + } + + [data-close-button] { + background: none; + border: none; + padding: 0; + cursor: pointer; + color: $text-muted; + @include transition-fast; + + @include hover { + color: $text-default; + } + } + } + + [data-sonner-toast][data-type='error'] { + border-color: $error-default; + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts new file mode 100644 index 0000000..3eb999d --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts @@ -0,0 +1,41 @@ +// =================== +// © AngelaMos | 2026 +// format.ts +// =================== + +const BYTE_UNITS = ['B', 'KB', 'MB', 'GB'] as const +const BYTES_PER_UNIT = 1024 +const DEFAULT_HEX_PAD = 8 +const DEFAULT_HASH_DISPLAY_LENGTH = 16 + +export function formatBytes(bytes: number): string { + let unitIndex = 0 + let value = bytes + while (value >= BYTES_PER_UNIT && unitIndex < BYTE_UNITS.length - 1) { + value /= BYTES_PER_UNIT + unitIndex++ + } + const decimals = unitIndex === 0 ? 0 : 2 + return `${value.toFixed(decimals)} ${BYTE_UNITS[unitIndex]}` +} + +export function formatHex(value: number, pad: number = DEFAULT_HEX_PAD): string { + return `0x${value.toString(16).toUpperCase().padStart(pad, '0')}` +} + +export function truncateHash( + hash: string, + length: number = DEFAULT_HASH_DISPLAY_LENGTH +): string { + if (hash.length <= length) return hash + return `${hash.slice(0, length)}\u2026` +} + +export async function copyToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text) + return true + } catch { + return false + } +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts new file mode 100644 index 0000000..58f981a --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts @@ -0,0 +1,7 @@ +// =================== +// © AngelaMos | 2026 +// index.ts +// =================== + +export * from './format' +export * from './shell.ui.store' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts new file mode 100644 index 0000000..e300578 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts @@ -0,0 +1,63 @@ +/** + * ©AngelaMos | 2026 + * shell.ui.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' | 'system' + +interface UIState { + theme: Theme + sidebarOpen: boolean + sidebarCollapsed: boolean + setTheme: (theme: Theme) => void + toggleSidebar: () => void + setSidebarOpen: (open: boolean) => void + toggleSidebarCollapsed: () => void +} + +export const useUIStore = create()( + devtools( + persist( + (set) => ({ + theme: 'dark', + sidebarOpen: false, + sidebarCollapsed: false, + + setTheme: (theme) => set({ theme }, false, 'ui/setTheme'), + + toggleSidebar: () => + set( + (state) => ({ sidebarOpen: !state.sidebarOpen }), + false, + 'ui/toggleSidebar' + ), + + setSidebarOpen: (open) => + set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'), + + toggleSidebarCollapsed: () => + set( + (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }), + false, + 'ui/toggleSidebarCollapsed' + ), + }), + { + name: 'ui-storage', + partialize: (state) => ({ + theme: state.theme, + sidebarCollapsed: state.sidebarCollapsed, + }), + } + ), + { name: 'UIStore' } + ) +) + +export const useTheme = (): Theme => useUIStore((s) => s.theme) +export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen) +export const useSidebarCollapsed = (): boolean => + useUIStore((s) => s.sidebarCollapsed) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx new file mode 100644 index 0000000..ac31951 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx @@ -0,0 +1,15 @@ +// =========================== +// ©AngelaMos | 2026 +// main.tsx +// =========================== + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.scss' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/analysis.module.scss b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/analysis.module.scss new file mode 100644 index 0000000..929be48 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/analysis.module.scss @@ -0,0 +1,1129 @@ +// =================== +// © AngelaMos | 2026 +// analysis.module.scss +// =================== + +@use '@/styles' as *; + +$content-max: 1200px; +$sidebar-width: 240px; + +.page { + min-height: 100vh; + min-height: 100dvh; + background: $bg-default; + display: flex; + flex-direction: column; + align-items: center; + padding: $space-4 $space-4 $space-8; + + @include breakpoint-up('md') { + padding: $space-6 $space-8 $space-12; + } +} + +.state { + min-height: 100vh; + min-height: 100dvh; + background: $bg-default; + @include flex-column-center; + gap: $space-3; +} + +.stateCode { + font-size: $font-size-5xl; + font-weight: $font-weight-black; + color: $text-muted; + letter-spacing: $tracking-tight; +} + +.stateLabel { + font-family: $font-mono; + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-lighter; +} + +.stateBack { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; + padding: $space-2 $space-4; + border: 1px solid $border-default; + margin-top: $space-4; + @include transition-fast; + + @include hover { + border-color: $accent; + color: $accent; + } +} + +.backLink { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; + @include transition-fast; + + @include hover { + color: $accent; + } +} + +.header { + width: 100%; + max-width: $content-max; + padding-bottom: $space-4; + border-bottom: 1px solid $border-default; + display: flex; + flex-direction: column; + gap: $space-3; +} + +.headerTop { + display: flex; + align-items: baseline; + justify-content: space-between; + flex-wrap: wrap; + gap: $space-3; +} + +.fileName { + font-size: $font-size-2xl; + font-weight: $font-weight-bold; + color: $text-default; + word-break: break-all; + + @include breakpoint-up('md') { + font-size: $font-size-3xl; + } +} + +.badges { + display: flex; + gap: $space-2; + flex-shrink: 0; +} + +.badge { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $text-lighter; + padding: $space-0-5 $space-2; + border: 1px solid $border-default; +} + +.headerMeta { + display: flex; + align-items: center; + gap: $space-5; + flex-wrap: wrap; +} + +.hashBtn { + display: flex; + align-items: center; + gap: $space-2; + @include transition-fast; + + @include hover { + .hashValue { + color: $text-default; + } + } +} + +.hashLabel { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.hashValue { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; + @include transition-fast; +} + +.metaItem { + display: flex; + align-items: center; + gap: $space-2; +} + +.metaLabel { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.metaValue { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; +} + +.scoreCard { + width: 100%; + max-width: $content-max; + padding: $space-6 0; + border-bottom: 1px solid $border-default; + display: flex; + flex-direction: column; + gap: $space-5; + + @include breakpoint-up('lg') { + flex-direction: row; + align-items: flex-start; + } +} + +.scoreMain { + display: flex; + align-items: baseline; + gap: $space-4; + flex-shrink: 0; +} + +.scoreNumber { + font-size: $font-size-5xl; + font-weight: $font-weight-black; + line-height: $line-height-none; + + @include breakpoint-up('md') { + font-size: $font-size-7xl; + } +} + +.scoreInfo { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.riskLabel { + font-family: $font-mono; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + text-transform: uppercase; + letter-spacing: $tracking-wider; +} + +.scoreSuffix { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.scoreBars { + flex: 1; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $space-2 $space-5; + + @include breakpoint-up('lg') { + grid-template-columns: repeat(4, 1fr); + } +} + +.scoreBar { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.scoreBarHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.scoreBarName { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $text-lighter; + @include truncate; +} + +.scoreBarValue { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; +} + +.scoreBarTrack { + height: 3px; + background: $border-muted; +} + +.scoreBarFill { + height: 100%; + background: $accent; + transition: width $duration-slow $ease-out; +} + +.tabBar { + width: 100%; + max-width: $content-max; + display: flex; + gap: $space-1; + border-bottom: 1px solid $border-default; + overflow-x: auto; + @include hide-scrollbar; +} + +.tab { + font-family: $font-mono; + font-size: $font-size-2xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $text-muted; + padding: $space-3 $space-4; + border-bottom: 2px solid transparent; + white-space: nowrap; + @include transition-fast; + + @include hover { + color: $text-lighter; + } + + &.tabActive { + color: $text-default; + border-bottom-color: $accent; + } +} + +.tabContent { + width: 100%; + max-width: $content-max; +} + +.tabPanel { + padding: $space-5 0; + display: flex; + flex-direction: column; + gap: $space-5; +} + +.noData { + font-family: $font-mono; + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $text-muted; + padding: $space-10 0; + text-align: center; +} + +.summaryGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $space-3; + + @include breakpoint-up('md') { + grid-template-columns: repeat(3, 1fr); + } +} + +.summaryCard { + padding: $space-4; + border: 1px solid $border-default; + display: flex; + flex-direction: column; + gap: $space-1; +} + +.summaryLabel { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.summaryValue { + font-family: $font-mono; + font-size: $font-size-sm; + color: $text-default; +} + +.summaryDetail { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-lighter; +} + +.overviewSection { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.sectionLabel { + font-family: $font-mono; + font-size: $font-size-2xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $accent; +} + +.anomalyList { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.anomalyItem { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; + padding: $space-2 $space-3; + border-left: 2px solid $error-default; + background: $bg-surface-75; +} + +.mitrePills { + display: flex; + flex-wrap: wrap; + gap: $space-2; +} + +.mitrePill { + display: flex; + align-items: center; + gap: $space-2; + padding: $space-1 $space-2-5; + border: 1px solid $border-default; + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + @include transition-fast; + + @include hover { + border-color: $accent; + color: $accent; + } +} + +.mitreId { + letter-spacing: $tracking-wider; + color: $text-default; +} + +.mitreName { + color: $text-lighter; +} + +.metaGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $space-2 $space-4; + + @include breakpoint-up('md') { + grid-template-columns: repeat(4, 1fr); + } +} + +.metaField { + display: flex; + flex-direction: column; + gap: $space-0-5; +} + +.metaFieldLabel { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.metaFieldValue { + font-family: $font-mono; + font-size: $font-size-sm; + color: $text-default; +} + +.libList { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $space-2; + padding-top: $space-2; +} + +.libItem { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; + padding: $space-0-5 $space-2; + border: 1px solid $border-muted; +} + +.tableWrap { + overflow-x: auto; + @include hide-scrollbar; +} + +.dataTable { + width: 100%; + border-collapse: collapse; + font-family: $font-mono; + font-size: $font-size-xs; + + th { + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; + padding: $space-2 $space-3; + text-align: left; + border-bottom: 1px solid $border-default; + white-space: nowrap; + font-weight: $font-weight-regular; + } + + td { + padding: $space-1-5 $space-3; + color: $text-lighter; + border-bottom: 1px solid $border-muted; + white-space: nowrap; + } +} + +.tableRow { + @include transition-fast; + + @include hover { + background: $bg-surface-75; + } + + &.suspicious { + td { + color: $error-light; + } + } + + &.blockBoundary { + td { + border-top: 2px solid $border-strong; + } + } +} + +.cellMono { + font-family: $font-mono; +} + +.cellRight { + text-align: right; +} + +.cellMnemonic { + font-family: $font-mono; + font-weight: $font-weight-bold; + color: $text-default; +} + +.perms { + display: flex; + gap: $space-0-5; + font-family: $font-mono; + font-size: $font-size-3xs; + letter-spacing: $tracking-wide; +} + +.permActive { + color: $text-default; +} + +.permInactive { + color: $text-muted; +} + +.permExec { + color: $accent; +} + +.collapseBtn { + display: flex; + align-items: center; + gap: $space-2; + width: 100%; + text-align: left; + @include transition-fast; + + @include hover { + .collapseIcon { + color: $text-lighter; + } + } +} + +.collapseIcon { + font-size: $font-size-3xs; + color: $text-muted; + @include transition-fast; +} + +.libraryGroup { + border: 1px solid $border-muted; +} + +.libraryHeader { + width: 100%; + display: flex; + align-items: center; + gap: $space-3; + padding: $space-2-5 $space-3; + text-align: left; + @include transition-fast; + + @include hover { + background: $bg-surface-75; + } +} + +.libraryName { + font-family: $font-mono; + font-size: $font-size-sm; + color: $text-default; + font-weight: $font-weight-medium; +} + +.libraryMeta { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + display: flex; + gap: $space-3; + margin-left: auto; +} + +.suspiciousCount { + color: $error-light; +} + +.tagRow { + display: flex; + flex-wrap: wrap; + gap: $space-1; +} + +.threatTag { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $error-light; + padding: $space-0-5 $space-1-5; + border: 1px solid $error-default; +} + +.alertCards { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.alertCard { + padding: $space-3 $space-4; + border: 1px solid $error-default; + background: $bg-surface-75; + display: flex; + flex-direction: column; + gap: $space-2; +} + +.alertHeader { + display: flex; + align-items: center; + gap: $space-3; + flex-wrap: wrap; +} + +.alertName { + font-family: $font-mono; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $text-default; +} + +.alertDesc { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; +} + +.alertApis { + display: flex; + flex-wrap: wrap; + gap: $space-1; +} + +.apiTag { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-lighter; + padding: $space-0-5 $space-1-5; + border: 1px solid $border-default; +} + +.severityBadge { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + padding: $space-0-5 $space-2; + border: 1px solid; +} + +.severityLow { + color: #84cc16; + border-color: #84cc16; +} + +.severityMedium { + color: #eab308; + border-color: #eab308; +} + +.severityHigh { + color: #f97316; + border-color: #f97316; +} + +.severityCritical { + color: #ef4444; + border-color: #ef4444; +} + +.categoryBadge { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $text-lighter; + padding: $space-0-5 $space-1-5; + border: 1px solid $border-default; +} + +.filterBar { + display: flex; + flex-wrap: wrap; + gap: $space-2; + align-items: center; +} + +.searchInput { + flex: 1; + min-width: 200px; + padding: $space-2 $space-3; + background: $bg-surface-75; + border: 1px solid $border-default; + font-family: $font-mono; + font-size: $font-size-xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $text-default; + @include transition-fast; + + &::placeholder { + color: $text-muted; + } + + &:focus { + border-color: $accent; + outline: none; + } +} + +.filterSelect { + padding: $space-2 $space-3; + background: $bg-surface-75; + border: 1px solid $border-default; + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $text-lighter; + cursor: pointer; + @include transition-fast; + + &:focus { + border-color: $accent; + outline: none; + } +} + +.filterToggle { + padding: $space-2 $space-3; + border: 1px solid $border-default; + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $text-muted; + @include transition-fast; + + @include hover { + border-color: $border-strong; + color: $text-lighter; + } + + &.filterActive { + border-color: $error-default; + color: $error-light; + } +} + +.filterCount { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + letter-spacing: $tracking-wide; +} + +.stringValue { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; + text-align: left; + word-break: break-all; + white-space: pre-wrap; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: $space-4; +} + +.pageBtn { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $text-lighter; + padding: $space-1-5 $space-3; + border: 1px solid $border-default; + @include transition-fast; + + @include hover { + border-color: $accent; + color: $accent; + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } +} + +.pageInfo { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + letter-spacing: $tracking-wide; +} + +.entropyOverall { + display: flex; + align-items: baseline; + gap: $space-3; + padding: $space-4 0; +} + +.entropyOverallLabel { + font-family: $font-mono; + font-size: $font-size-2xs; + text-transform: uppercase; + letter-spacing: $tracking-widest; + color: $text-muted; +} + +.entropyOverallValue { + font-size: $font-size-3xl; + font-weight: $font-weight-bold; + color: $text-default; +} + +.entropyOverallScale { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-muted; +} + +.packingAlert { + padding: $space-4; + border: 1px solid $error-default; + background: $bg-surface-75; + display: flex; + flex-direction: column; + gap: $space-2; +} + +.packingTitle { + font-family: $font-mono; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + text-transform: uppercase; + letter-spacing: $tracking-wider; + color: $error-light; +} + +.packingName { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-default; +} + +.packingIndicator { + display: flex; + gap: $space-3; + font-family: $font-mono; + font-size: $font-size-xs; +} + +.packingType { + color: $text-muted; + text-transform: uppercase; + letter-spacing: $tracking-wide; + flex-shrink: 0; +} + +.packingEvidence { + color: $text-lighter; +} + +.entropyBars { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.entropyRow { + display: flex; + flex-direction: column; + gap: $space-1; + padding: $space-2 0; + border-bottom: 1px solid $border-muted; + + &.anomalous { + border-left: 2px solid $error-default; + padding-left: $space-3; + } +} + +.entropyMeta { + display: flex; + justify-content: space-between; + align-items: center; +} + +.entropySectionName { + font-family: $font-mono; + font-size: $font-size-sm; + color: $text-default; +} + +.entropyClassification { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wider; +} + +.entropyBarWrap { + display: flex; + align-items: center; + gap: $space-3; +} + +.entropyBarTrack { + flex: 1; + height: 6px; + background: $border-muted; +} + +.entropyBarFill { + height: 100%; + transition: width $duration-slow $ease-out; +} + +.entropyValue { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-lighter; + min-width: 36px; + text-align: right; +} + +.entropyDetails { + display: flex; + align-items: center; + gap: $space-3; + flex-wrap: wrap; +} + +.entropyDetail { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + letter-spacing: $tracking-wide; +} + +.entropyFlags { + display: flex; + gap: $space-1; +} + +.entropyFlag { + font-family: $font-mono; + font-size: $font-size-3xs; + text-transform: uppercase; + letter-spacing: $tracking-wide; + color: $error-light; + padding: $space-0-5 $space-1-5; + border: 1px solid $error-default; +} + +.disasmLayout { + display: flex; + gap: $space-4; + min-height: 500px; + + @include breakpoint-down('md') { + flex-direction: column; + } +} + +.fnSidebar { + width: $sidebar-width; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: $space-2; + border-right: 1px solid $border-default; + padding-right: $space-4; + + @include breakpoint-down('md') { + width: 100%; + border-right: none; + border-bottom: 1px solid $border-default; + padding-right: 0; + padding-bottom: $space-4; + max-height: 200px; + } +} + +.fnList { + overflow-y: auto; + display: flex; + flex-direction: column; + gap: $space-0-5; + @include hide-scrollbar; +} + +.fnItem { + display: flex; + flex-direction: column; + gap: $space-0-5; + padding: $space-1-5 $space-2; + text-align: left; + @include transition-fast; + + @include hover { + background: $bg-surface-75; + } + + &.fnActive { + background: $bg-surface-100; + border-left: 2px solid $accent; + padding-left: calc($space-2 - 2px); + } + + &.fnEntry { + .fnAddr { + color: $accent; + } + } +} + +.fnAddr { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + letter-spacing: $tracking-wide; +} + +.fnName { + font-family: $font-mono; + font-size: $font-size-xs; + color: $text-default; + @include truncate; +} + +.fnMeta { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; +} + +.disasmMain { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: $space-4; +} + +.fnHeader { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.fnHeaderName { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $text-default; + font-family: $font-mono; +} + +.fnHeaderMeta { + font-family: $font-mono; + font-size: $font-size-3xs; + color: $text-muted; + letter-spacing: $tracking-wide; +} + +.cfgContainer { + overflow-x: auto; + padding: $space-4; + border: 1px solid $border-muted; + background: $bg-surface-75; +} + +.cfgSvg { + max-width: 100%; + height: auto; + min-height: 200px; +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx new file mode 100644 index 0000000..1448819 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx @@ -0,0 +1,195 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import { toast } from 'sonner' +import type { AnalysisResponse } from '@/api' +import { useAnalysis } from '@/api' +import { RISK_LEVEL_COLORS, ROUTES } from '@/config' +import { copyToClipboard, formatBytes, formatHex, truncateHash } from '@/core/lib' +import styles from './analysis.module.scss' +import { TabDisassembly } from './tab-disassembly' +import { TabEntropy } from './tab-entropy' +import { TabHeaders } from './tab-headers' +import { TabImports } from './tab-imports' +import { TabOverview } from './tab-overview' +import { TabStrings } from './tab-strings' + +type TabId = + | 'overview' + | 'headers' + | 'imports' + | 'strings' + | 'entropy' + | 'disassembly' + +const TABS: readonly { id: TabId; label: string }[] = [ + { id: 'overview', label: 'OVERVIEW' }, + { id: 'headers', label: 'HEADERS' }, + { id: 'imports', label: 'IMPORTS' }, + { id: 'strings', label: 'STRINGS' }, + { id: 'entropy', label: 'ENTROPY' }, + { id: 'disassembly', label: 'DISASM' }, +] as const + +function renderTab(tab: TabId, data: AnalysisResponse): React.ReactElement { + switch (tab) { + case 'overview': + return + case 'headers': + return + case 'imports': + return + case 'strings': + return + case 'entropy': + return + case 'disassembly': + return + } +} + +function ScoreBar({ + name, + score, + maxScore, +}: { + name: string + score: number + maxScore: number +}): React.ReactElement { + const pct = maxScore > 0 ? (score / maxScore) * 100 : 0 + return ( +
+
+ {name} + + {score}/{maxScore} + +
+
+
+
+
+ ) +} + +export function Component(): React.ReactElement { + const { slug = '' } = useParams<{ slug: string }>() + const { data, isLoading, isError } = useAnalysis(slug) + const [activeTab, setActiveTab] = useState('overview') + + if (isLoading) { + return ( +
+ ANALYZING SPECIMEN\u2026 +
+ ) + } + + if (isError || !data) { + return ( +
+ 404 + SPECIMEN NOT FOUND + + NEW ANALYSIS + +
+ ) + } + + const riskColor = data.risk_level + ? (RISK_LEVEL_COLORS[data.risk_level] ?? '#888') + : '#888' + + const handleCopyHash = async () => { + const ok = await copyToClipboard(data.sha256) + if (ok) toast.success('SHA-256 copied') + } + + return ( +
+
+ + AXUMORTEM + +
+

{data.file_name}

+
+ {data.format} + {data.architecture} + {formatBytes(data.file_size)} +
+
+
+ + {data.entry_point !== null && ( + + ENTRY + + {formatHex(data.entry_point)} + + + )} +
+
+ + {data.passes.threat && ( +
+
+ + {data.threat_score ?? 0} + +
+ + {data.risk_level ?? 'UNKNOWN'} + + / 100 THREAT SCORE +
+
+ {data.passes.threat.categories.length > 0 && ( +
+ {data.passes.threat.categories.map((cat) => ( + + ))} +
+ )} +
+ )} + + + +
{renderTab(activeTab, data)}
+
+ ) +} + +Component.displayName = 'Analysis' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx new file mode 100644 index 0000000..3f4eeba --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx @@ -0,0 +1,304 @@ +// =================== +// © AngelaMos | 2026 +// tab-disassembly.tsx +// =================== + +import { layout as dagreLayout, Graph } from '@dagrejs/dagre' +import { useMemo, useState } from 'react' +import type { + AnalysisResponse, + CfgEdge, + CfgEdgeType, + CfgNode, + FunctionInfo, +} from '@/api' +import { formatHex } from '@/core/lib' +import styles from './analysis.module.scss' + +const CFG_NODE_WIDTH = 160 +const CFG_NODE_HEIGHT = 40 +const CFG_RANK_SEP = 60 +const CFG_NODE_SEP = 30 + +const CFG_EDGE_COLORS: Record = { + Fallthrough: '#6b7280', + ConditionalTrue: '#22c55e', + ConditionalFalse: '#ef4444', + Unconditional: '#3b82f6', + Call: '#a855f7', +} + +interface LayoutNode { + id: number + x: number + y: number + label: string + instructionCount: number +} + +interface LayoutEdge { + from: { x: number; y: number } + to: { x: number; y: number } + color: string +} + +interface CfgLayout { + nodes: LayoutNode[] + edges: LayoutEdge[] + width: number + height: number +} + +function layoutCfg(nodes: CfgNode[], edges: CfgEdge[]): CfgLayout { + const g = new Graph() + g.setGraph({ + rankdir: 'TB', + ranksep: CFG_RANK_SEP, + nodesep: CFG_NODE_SEP, + }) + g.setDefaultEdgeLabel(() => ({})) + + for (const node of nodes) { + g.setNode(String(node.id), { + width: CFG_NODE_WIDTH, + height: CFG_NODE_HEIGHT, + }) + } + + for (const edge of edges) { + g.setEdge(String(edge.from), String(edge.to)) + } + + dagreLayout(g) + + const layoutNodes: LayoutNode[] = nodes.map((node) => { + const pos = g.node(String(node.id)) + return { + id: node.id, + x: pos.x, + y: pos.y, + label: node.label, + instructionCount: node.instruction_count, + } + }) + + const layoutEdges: LayoutEdge[] = edges.map((edge) => { + const fromPos = g.node(String(edge.from)) + const toPos = g.node(String(edge.to)) + return { + from: { x: fromPos.x, y: fromPos.y + CFG_NODE_HEIGHT / 2 }, + to: { x: toPos.x, y: toPos.y - CFG_NODE_HEIGHT / 2 }, + color: CFG_EDGE_COLORS[edge.edge_type], + } + }) + + const graphInfo = g.graph() + return { + nodes: layoutNodes, + edges: layoutEdges, + width: (graphInfo.width ?? 400) + CFG_NODE_WIDTH, + height: (graphInfo.height ?? 300) + CFG_NODE_HEIGHT, + } +} + +function CfgGraph({ + nodes, + edges, +}: { + nodes: CfgNode[] + edges: CfgEdge[] +}): React.ReactElement { + const layout = useMemo(() => layoutCfg(nodes, edges), [nodes, edges]) + const padX = CFG_NODE_WIDTH / 2 + const padY = CFG_NODE_HEIGHT / 2 + + return ( +
+ + Control flow graph + + + + + + + {layout.edges.map((edge, i) => ( + + ))} + + {layout.nodes.map((node) => ( + + + + {node.label} + + + {node.instructionCount} insn + + + ))} + +
+ ) +} + +function InstructionTable({ fn }: { fn: FunctionInfo }): React.ReactElement { + return ( +
+ + + + + + + + + + + {fn.basic_blocks.map((block, blockIdx) => ( + <> + {block.instructions.map((insn, i) => ( + 0 ? styles.blockBoundary : ''}`} + > + + + + + + ))} + + ))} + +
ADDRESSBYTESMNEMONICOPERANDS
{formatHex(insn.address)} + {insn.bytes + .map((b) => b.toString(16).padStart(2, '0')) + .join(' ')} + {insn.mnemonic}{insn.operands}
+
+ ) +} + +export function TabDisassembly({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const disasm = data.passes.disassembly + const [selectedAddr, setSelectedAddr] = useState(null) + + if (!disasm) { + return ( +
+ + Disassembly is only available for x86 and x86_64 binaries + +
+ ) + } + + const selectedFn = + disasm.functions.find((f) => f.address === selectedAddr) ?? + disasm.functions[0] ?? + null + + return ( +
+
+ + +
+ {selectedFn ? ( + <> +
+ + {selectedFn.name ?? `sub_${selectedFn.address.toString(16)}`} + + + {formatHex(selectedFn.address)} / {selectedFn.size} bytes /{' '} + {selectedFn.instruction_count} instructions /{' '} + {selectedFn.basic_blocks.length} blocks + +
+ + {selectedFn.cfg.nodes.length > 0 && ( +
+ CONTROL FLOW GRAPH + +
+ )} + + ) : ( + No functions found + )} +
+
+
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx new file mode 100644 index 0000000..bad93d5 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx @@ -0,0 +1,110 @@ +// =================== +// © AngelaMos | 2026 +// tab-entropy.tsx +// =================== + +import type { AnalysisResponse, SectionEntropy } from '@/api' +import { ENTROPY_CLASSIFICATION_COLORS } from '@/config' +import styles from './analysis.module.scss' + +const MAX_ENTROPY = 8 + +function EntropyBar({ + section, +}: { + section: SectionEntropy +}): React.ReactElement { + const pct = (section.entropy / MAX_ENTROPY) * 100 + const color = ENTROPY_CLASSIFICATION_COLORS[section.classification] ?? '#888' + + return ( +
+
+ {section.name} + + {section.classification} + +
+
+
+
+
+ {section.entropy.toFixed(2)} +
+
+ + SIZE {section.size.toLocaleString()} + + + V/R {section.virtual_to_raw_ratio.toFixed(2)} + + {section.flags.length > 0 && ( +
+ {section.flags.map((flag) => ( + + {flag} + + ))} +
+ )} +
+
+ ) +} + +export function TabEntropy({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const ent = data.passes.entropy + + if (!ent) { + return ( +
+ No entropy data available +
+ ) + } + + return ( +
+
+ OVERALL ENTROPY + + {ent.overall_entropy.toFixed(4)} + + / {MAX_ENTROPY} +
+ + {ent.packing_detected && ( +
+ PACKING DETECTED + {ent.packer_name && ( + {ent.packer_name} + )} + {ent.packing_indicators.map((ind, i) => ( +
+ {ind.indicator_type} + {ind.description} +
+ ))} +
+ )} + +
+ PER-SECTION ENTROPY +
+ {ent.sections.map((sec) => ( + + ))} +
+
+
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx new file mode 100644 index 0000000..8409cbd --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx @@ -0,0 +1,315 @@ +// =================== +// © AngelaMos | 2026 +// tab-headers.tsx +// =================== + +import { useState } from 'react' +import type { AnalysisResponse, SectionInfo, SegmentInfo } from '@/api' +import { formatHex } from '@/core/lib' +import styles from './analysis.module.scss' + +function PermsBadge({ + r, + w, + x, +}: { + r: boolean + w: boolean + x: boolean +}): React.ReactElement { + return ( + + R + W + X + + ) +} + +function SectionRow({ section }: { section: SectionInfo }): React.ReactElement { + return ( + + {section.name} + {formatHex(section.virtual_address)} + + {section.virtual_size.toLocaleString()} + + {section.raw_size.toLocaleString()} + + + + + ) +} + +function SegmentRow({ segment }: { segment: SegmentInfo }): React.ReactElement { + return ( + + {segment.name ?? '\u2014'} + {formatHex(segment.virtual_address)} + + {segment.virtual_size.toLocaleString()} + + {segment.file_size.toLocaleString()} + + + + + ) +} + +export function TabHeaders({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const [showSegments, setShowSegments] = useState(false) + const fmt = data.passes.format + + if (!fmt) { + return ( +
+ No format data available +
+ ) + } + + return ( +
+
+ FORMAT INFO +
+
+ FORMAT + {fmt.format} +
+
+ ARCH + + {typeof fmt.architecture === 'string' + ? fmt.architecture + : fmt.architecture.Other} + +
+
+ BITS + {fmt.bits} +
+
+ ENDIAN + {fmt.endianness} +
+
+ ENTRY + + {formatHex(fmt.entry_point)} + +
+
+ STRIPPED + + {fmt.is_stripped ? 'YES' : 'NO'} + +
+
+ PIE + + {fmt.is_pie ? 'YES' : 'NO'} + +
+
+ DEBUG + + {fmt.has_debug_info ? 'YES' : 'NO'} + +
+
+
+ + {fmt.pe_info && ( +
+ PE INFO +
+
+ IMAGE BASE + + {formatHex(fmt.pe_info.image_base)} + +
+
+ SUBSYSTEM + + {fmt.pe_info.subsystem} + +
+
+ LINKER + + {fmt.pe_info.linker_version} + +
+
+ ASLR + + {fmt.pe_info.dll_characteristics.aslr ? 'YES' : 'NO'} + +
+
+ DEP + + {fmt.pe_info.dll_characteristics.dep ? 'YES' : 'NO'} + +
+
+ CFG + + {fmt.pe_info.dll_characteristics.cfg ? 'YES' : 'NO'} + +
+
+
+ )} + + {fmt.elf_info && ( +
+ ELF INFO +
+
+ OS ABI + {fmt.elf_info.os_abi} +
+
+ TYPE + + {fmt.elf_info.elf_type} + +
+
+ RELRO + + {fmt.elf_info.gnu_relro ? 'FULL' : 'NO'} + +
+
+ BIND NOW + + {fmt.elf_info.bind_now ? 'YES' : 'NO'} + +
+
+ NX STACK + + {fmt.elf_info.stack_executable ? 'EXEC' : 'NO-EXEC'} + +
+
+ {fmt.elf_info.needed_libraries.length > 0 && ( +
+ LIBRARIES + {fmt.elf_info.needed_libraries.map((lib) => ( + + {lib} + + ))} +
+ )} +
+ )} + + {fmt.macho_info && ( +
+ MACH-O INFO +
+
+ FILE TYPE + + {fmt.macho_info.file_type} + +
+
+ UNIVERSAL + + {fmt.macho_info.is_universal ? 'YES' : 'NO'} + +
+
+ CODE SIGN + + {fmt.macho_info.has_code_signature ? 'YES' : 'NO'} + +
+
+
+ )} + +
+ + SECTIONS ({fmt.sections.length}) + +
+ + + + + + + + + + + + {fmt.sections.map((sec) => ( + + ))} + +
NAMEVADDRVSIZERAW SIZEPERMS
+
+
+ + {fmt.segments.length > 0 && ( +
+ + {showSegments && ( +
+ + + + + + + + + + + + {fmt.segments.map((seg, i) => ( + + ))} + +
NAMEVADDRVSIZEFSIZEPERMS
+
+ )} +
+ )} +
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx new file mode 100644 index 0000000..174a301 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx @@ -0,0 +1,205 @@ +// =================== +// © AngelaMos | 2026 +// tab-imports.tsx +// =================== + +import { useState } from 'react' +import type { AnalysisResponse, ImportEntry } from '@/api' +import { formatHex } from '@/core/lib' +import styles from './analysis.module.scss' + +function ImportRow({ entry }: { entry: ImportEntry }): React.ReactElement { + return ( + + {entry.function} + + {entry.address !== null ? formatHex(entry.address) : '\u2014'} + + + {entry.ordinal !== null ? `#${entry.ordinal}` : '\u2014'} + + + {entry.threat_tags.length > 0 && ( +
+ {entry.threat_tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + + ) +} + +function LibraryGroup({ + library, + imports, + isOpen, + onToggle, +}: { + library: string + imports: ImportEntry[] + isOpen: boolean + onToggle: () => void +}): React.ReactElement { + const suspiciousCount = imports.filter((i) => i.is_suspicious).length + return ( +
+ + {isOpen && ( +
+ + + + + + + + + + + {imports.map((entry, i) => ( + + ))} + +
FUNCTIONADDRESSORDINALTAGS
+
+ )} +
+ ) +} + +export function TabImports({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const imp = data.passes.imports + const [openLibs, setOpenLibs] = useState>(new Set()) + + if (!imp) { + return ( +
+ No import data available +
+ ) + } + + const toggleLib = (lib: string) => { + setOpenLibs((prev) => { + const next = new Set(prev) + if (next.has(lib)) next.delete(lib) + else next.add(lib) + return next + }) + } + + const importsByLib = new Map() + for (const entry of imp.imports) { + const list = importsByLib.get(entry.library) ?? [] + list.push(entry) + importsByLib.set(entry.library, list) + } + + return ( +
+ {imp.suspicious_combinations.length > 0 && ( +
+ SUSPICIOUS COMBINATIONS +
+ {imp.suspicious_combinations.map((combo) => ( +
+
+ {combo.name} + {combo.mitre_id} + + {combo.severity} + +
+ {combo.description} +
+ {combo.apis.map((api) => ( + + {api} + + ))} +
+
+ ))} +
+
+ )} + +
+ + IMPORTS ({imp.statistics.total_imports}) + + {Array.from(importsByLib.entries()).map(([lib, imports]) => ( + toggleLib(lib)} + /> + ))} +
+ + {imp.exports.length > 0 && ( +
+ + EXPORTS ({imp.exports.length}) + +
+ + + + + + + + + + + {imp.exports.map((exp, i) => ( + + + + + + + ))} + +
NAMEADDRESSORDINALFORWARD
{exp.name ?? '\u2014'}{formatHex(exp.address)} + {exp.ordinal !== null ? `#${exp.ordinal}` : '\u2014'} + + {exp.forward_target ?? '\u2014'} +
+
+
+ )} +
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx new file mode 100644 index 0000000..77ae4a2 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx @@ -0,0 +1,144 @@ +// =================== +// © AngelaMos | 2026 +// tab-overview.tsx +// =================== + +import type { AnalysisResponse } from '@/api' +import styles from './analysis.module.scss' + +const MITRE_BASE_URL = 'https://attack.mitre.org/techniques/' + +function formatMitreUrl(id: string): string { + return `${MITRE_BASE_URL}${id.replace('.', '/')}` +} + +export function TabOverview({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const { passes } = data + + return ( +
+
+ {passes.format && ( +
+ FORMAT + + {passes.format.format} / {passes.format.bits}-bit + + + {passes.format.sections.length} sections,{' '} + {passes.format.segments.length} segments + +
+ )} + + {passes.imports && ( +
+ IMPORTS + + {passes.imports.statistics.total_imports} across{' '} + {passes.imports.statistics.library_count} libraries + + + {passes.imports.statistics.suspicious_count} suspicious + +
+ )} + + {passes.strings && ( +
+ STRINGS + + {passes.strings.statistics.total} extracted + + + {passes.strings.statistics.suspicious_count} suspicious + +
+ )} + + {passes.entropy && ( +
+ ENTROPY + + {passes.entropy.overall_entropy.toFixed(2)} overall + + + {passes.entropy.packing_detected + ? `Packing detected${passes.entropy.packer_name ? `: ${passes.entropy.packer_name}` : ''}` + : 'No packing detected'} + +
+ )} + + {passes.disassembly && ( +
+ DISASSEMBLY + + {passes.disassembly.total_functions} functions + + + {passes.disassembly.total_instructions} instructions + +
+ )} + + {passes.threat && ( +
+ YARA + + {passes.threat.yara_matches.length} rule matches + + {passes.threat.summary} +
+ )} +
+ + {passes.format && passes.format.anomalies.length > 0 && ( +
+ ANOMALIES +
+ {passes.format.anomalies.map((anomaly, i) => ( +
+ {typeof anomaly === 'string' ? ( + {anomaly} + ) : ( + Object.entries(anomaly).map(([key, val]) => ( + + {key}: {String(val)} + + )) + )} +
+ ))} +
+
+ )} + + {passes.threat && passes.threat.mitre_techniques.length > 0 && ( +
+ MITRE ATT&CK +
+ {passes.threat.mitre_techniques.map((technique) => ( + + {technique.technique_id} + + {technique.technique_name} + + + ))} +
+
+ )} +
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx new file mode 100644 index 0000000..f7a3438 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx @@ -0,0 +1,233 @@ +// =================== +// © AngelaMos | 2026 +// tab-strings.tsx +// =================== + +import { useMemo, useState } from 'react' +import type { + AnalysisResponse, + ExtractedString, + StringCategory, + StringEncoding, +} from '@/api' +import { formatHex } from '@/core/lib' +import styles from './analysis.module.scss' + +const ENCODING_OPTIONS: readonly ('All' | StringEncoding)[] = [ + 'All', + 'Ascii', + 'Utf8', + 'Utf16Le', +] as const + +const CATEGORY_OPTIONS: readonly ('All' | StringCategory)[] = [ + 'All', + 'Url', + 'IpAddress', + 'FilePath', + 'RegistryKey', + 'ShellCommand', + 'CryptoWallet', + 'Email', + 'SuspiciousApi', + 'PackerSignature', + 'DebugArtifact', + 'AntiAnalysis', + 'PersistencePath', + 'EncodedData', + 'Generic', +] as const + +const PAGE_SIZE = 50 + +function StringRow({ + str, + expanded, + onToggle, +}: { + str: ExtractedString + expanded: boolean + onToggle: () => void +}): React.ReactElement { + const MAX_DISPLAY = 80 + const needsTruncate = str.value.length > MAX_DISPLAY + const display = + expanded || !needsTruncate + ? str.value + : `${str.value.slice(0, MAX_DISPLAY)}\u2026` + + return ( + + {formatHex(str.offset)} + + + + {str.encoding} + + {str.category} + + {str.section ?? '\u2014'} + + ) +} + +export function TabStrings({ + data, +}: { + data: AnalysisResponse +}): React.ReactElement { + const str = data.passes.strings + const [search, setSearch] = useState('') + const [encoding, setEncoding] = useState<'All' | StringEncoding>('All') + const [category, setCategory] = useState<'All' | StringCategory>('All') + const [suspiciousOnly, setSuspiciousOnly] = useState(false) + const [page, setPage] = useState(0) + const [expandedRows, setExpandedRows] = useState>(new Set()) + + const filtered = useMemo(() => { + if (!str) return [] + return str.strings.filter((s) => { + if (search && !s.value.toLowerCase().includes(search.toLowerCase())) + return false + if (encoding !== 'All' && s.encoding !== encoding) return false + if (category !== 'All' && s.category !== category) return false + if (suspiciousOnly && !s.is_suspicious) return false + return true + }) + }, [str, search, encoding, category, suspiciousOnly]) + + if (!str) { + return ( +
+ No string data available +
+ ) + } + + const totalPages = Math.ceil(filtered.length / PAGE_SIZE) + const pageStrings = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE) + + const toggleRow = (offset: number) => { + setExpandedRows((prev) => { + const next = new Set(prev) + if (next.has(offset)) next.delete(offset) + else next.add(offset) + return next + }) + } + + return ( +
+
+ { + setSearch(e.target.value) + setPage(0) + }} + /> + + + +
+ + + {filtered.length} / {str.statistics.total} strings + + +
+ + + + + + + + + + + + {pageStrings.map((s) => ( + toggleRow(s.offset)} + /> + ))} + +
OFFSETVALUEENCODINGCATEGORYSECTION
+
+ + {totalPages > 1 && ( +
+ + + {page + 1} / {totalPages} + + +
+ )} +
+ ) +} diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx new file mode 100644 index 0000000..8779d86 --- /dev/null +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx @@ -0,0 +1,205 @@ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +import { useCallback, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { toast } from 'sonner' +import { useUpload } from '@/api' +import { formatBytes } from '@/core/lib' +import styles from './landing.module.scss' + +const HEX_OFFSETS = Array.from( + { length: 16 }, + (_, i) => `0x${(i * 16).toString(16).toUpperCase().padStart(4, '0')}` +) + +const ANALYSIS_PASSES = [ + { id: '01', name: 'FORMAT' }, + { id: '02', name: 'IMPORTS' }, + { id: '03', name: 'STRINGS' }, + { id: '04', name: 'ENTROPY' }, + { id: '05', name: 'DISASM' }, + { id: '06', name: 'THREAT' }, +] as const + +export function Component(): React.ReactElement { + const [file, setFile] = useState(null) + const [isDragging, setIsDragging] = useState(false) + const inputRef = useRef(null) + const navigate = useNavigate() + const upload = useUpload() + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.dataTransfer.dropEffect = 'copy' + }, []) + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + const related = e.relatedTarget as Node | null + if (related && e.currentTarget.contains(related)) return + setIsDragging(false) + }, []) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + const droppedFile = e.dataTransfer.files[0] + if (droppedFile) setFile(droppedFile) + }, []) + + const handleFileSelect = useCallback( + (e: React.ChangeEvent) => { + const selected = e.target.files?.[0] + if (selected) setFile(selected) + }, + [] + ) + + const handleSubmit = useCallback(() => { + if (!file) return + upload.mutate(file, { + onSuccess: (data) => navigate(`/analysis/${data.slug}`), + onError: () => toast.error('Failed to analyze binary'), + }) + }, [file, upload, navigate]) + + const handleClear = useCallback(() => { + setFile(null) + upload.reset() + if (inputRef.current) inputRef.current.value = '' + }, [upload]) + + return ( +
+ + +