From 6eb64d36da00aae1b8f156611991ab5f64a52618 Mon Sep 17 00:00:00 2001 From: ParrotXray Date: Sat, 23 May 2026 11:29:23 +0800 Subject: [PATCH] feat/account-api (#13) * wip * feat: change ebpf build path * feat: change ebpf build path * feat: add SQLCipher account DB, Argon2 password hashing, and JWT auth * feat: add graceful shutdown on SIGINT * feat: adjust code with rustfmt * docs: edit README.md --- Cargo.lock | 1101 ++++++++++-------- README.md | 44 +- TODO | 1 + common/src/model/event.rs | 1 - config.toml | 7 + lib/ebpf/info.txt | 1 + {onnxruntime => lib/onnxruntime}/info.txt | 0 macros/src/log.rs | 4 +- mantis-frontend | 2 +- mantis/Cargo.toml | 12 +- mantis/build.rs | 39 +- mantis/src/core/app_state.rs | 22 + mantis/src/core/ebpf/access_control.rs | 2 +- mantis/src/core/ebpf/mod.rs | 10 +- mantis/src/core/ebpf/service.rs | 2 +- mantis/src/core/ebpf/statistics.rs | 24 +- mantis/src/core/ebpf/xsk_manager.rs | 14 +- mantis/src/core/infrastructure/app_config.rs | 2 +- mantis/src/core/infrastructure/app_db.rs | 115 ++ mantis/src/core/infrastructure/geoip.rs | 47 +- mantis/src/core/infrastructure/health.rs | 58 +- mantis/src/core/infrastructure/mod.rs | 39 +- mantis/src/core/mod.rs | 1 + mantis/src/core/system.rs | 103 +- mantis/src/detection/fusion.rs | 24 +- mantis/src/detection/ml/aggregator.rs | 22 +- mantis/src/detection/ml/config_loader.rs | 20 +- mantis/src/detection/ml/engine.rs | 37 +- mantis/src/detection/ml/feature_extractor.rs | 31 +- mantis/src/detection/ml/flow_tracker.rs | 49 +- mantis/src/detection/ml/inference.rs | 30 +- mantis/src/detection/ml/mod.rs | 12 +- mantis/src/detection/ml/model_loader.rs | 13 +- mantis/src/detection/ml/traffic_logger.rs | 4 +- mantis/src/detection/mod.rs | 2 +- mantis/src/detection/suricata/engine.rs | 102 +- mantis/src/detection/suricata/mod.rs | 2 +- mantis/src/detection/suricata/output.rs | 6 +- mantis/src/main.rs | 8 +- mantis/src/model/config.rs | 33 +- mantis/src/model/error/auth.rs | 21 + mantis/src/model/error/ebpf.rs | 1 - mantis/src/model/error/http.rs | 5 +- mantis/src/model/error/misc.rs | 2 +- mantis/src/model/error/ml.rs | 2 +- mantis/src/model/error/mod.rs | 14 +- mantis/src/model/geo_stats.rs | 2 +- mantis/src/model/ip_address.rs | 24 +- mantis/src/model/log/auth.rs | 18 + mantis/src/model/log/ebpf.rs | 4 +- mantis/src/model/log/ml.rs | 2 +- mantis/src/model/log/mod.rs | 9 +- mantis/src/model/log/suricata.rs | 2 +- mantis/src/model/log/system.rs | 2 +- mantis/src/model/ml_detection.rs | 2 +- mantis/src/model/mod.rs | 4 +- mantis/src/utils/ip_address.rs | 9 +- mantis/src/utils/logging.rs | 6 +- mantis/src/utils/mod.rs | 8 +- mantis/src/utils/packet_parser.rs | 2 +- mantis/src/web/api/auth.rs | 105 ++ mantis/src/web/api/control/access_control.rs | 124 +- mantis/src/web/api/control/mod.rs | 14 +- mantis/src/web/api/control/service.rs | 330 +++--- mantis/src/web/api/control/statistics.rs | 98 +- mantis/src/web/api/default.rs | 39 +- mantis/src/web/api/detection_alert.rs | 26 +- mantis/src/web/api/health.rs | 45 +- mantis/src/web/api/misc.rs | 16 +- mantis/src/web/api/mod.rs | 1 + mantis/src/web/middleware/auth.rs | 43 + mantis/src/web/middleware/mod.rs | 1 + mantis/src/web/mod.rs | 1 + mantis/src/web/websocket/alert_websocket.rs | 104 +- mantis/src/web/websocket/flow_websocket.rs | 216 +--- mantis/src/web/websocket/health_websocket.rs | 104 +- mantis/src/web/websocket/mod.rs | 2 +- 77 files changed, 1888 insertions(+), 1566 deletions(-) create mode 100644 lib/ebpf/info.txt rename {onnxruntime => lib/onnxruntime}/info.txt (100%) create mode 100644 mantis/src/core/app_state.rs create mode 100644 mantis/src/core/infrastructure/app_db.rs create mode 100644 mantis/src/model/error/auth.rs create mode 100644 mantis/src/model/log/auth.rs create mode 100644 mantis/src/web/api/auth.rs create mode 100644 mantis/src/web/middleware/auth.rs create mode 100644 mantis/src/web/middleware/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c9238c6..2b47bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,257 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.1.4", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.98", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.8", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash 0.1.4", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.0", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "actix-ws" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "decf53c3cdd63dd6f289980b430238f9a2f6d19f8bce8e418272e08d3da43f0f" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "bytestring", - "futures-core", - "futures-sink", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "adler2" version = "2.0.0" @@ -280,21 +29,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -328,18 +62,103 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "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 = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "aya" version = "0.13.1" @@ -473,6 +292,12 @@ 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 = "bindgen" version = "0.72.1" @@ -514,6 +339,15 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -523,27 +357,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -562,15 +375,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - [[package]] name = "camino" version = "1.1.12" @@ -610,8 +414,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -670,17 +472,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-error" version = "0.0.0" @@ -794,9 +585,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -812,27 +603,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", - "unicode-xid", -] - [[package]] name = "digest" version = "0.10.7" @@ -841,6 +611,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -894,15 +665,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[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 = "env_home" version = "0.1.0" @@ -925,6 +687,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "filetime" version = "0.2.27" @@ -952,12 +726,6 @@ dependencies = [ "miniz_oxide", ] -[[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.4" @@ -1085,8 +853,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1097,35 +867,29 @@ checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.3+wasi-0.2.4", ] +[[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 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.7.1" @@ -1170,16 +934,20 @@ dependencies = [ ] [[package]] -name = "http" -version = "0.2.12" +name = "hashlink" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "bytes", - "fnv", - "itoa", + "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.4.0" @@ -1190,6 +958,29 @@ dependencies = [ "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.0" @@ -1202,6 +993,41 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "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" @@ -1344,6 +1170,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1365,12 +1197,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "2.13.0" @@ -1379,6 +1205,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1440,15 +1268,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.94" @@ -1459,6 +1278,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kstring" version = "2.0.2" @@ -1469,18 +1303,18 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libbpf-sys" version = "1.5.1+v1.5.1" @@ -1535,6 +1369,18 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "openssl-sys", + "pkg-config", + "vcpkg", +] + [[package]] name = "libxdp-sys" version = "0.2.3+1.5.6" @@ -1612,23 +1458,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.14" @@ -1666,10 +1495,8 @@ dependencies = [ name = "mantis" version = "0.1.0" dependencies = [ - "actix", - "actix-cors", - "actix-web", - "actix-ws", + "argon2", + "axum", "aya", "aya-log", "cargo_metadata", @@ -1680,6 +1507,7 @@ dependencies = [ "dotenvy", "futures", "futures-util", + "jsonwebtoken", "libc", "lru", "macros", @@ -1690,19 +1518,22 @@ dependencies = [ "ort", "ort-tract", "parking_lot", + "rusqlite", "rust-embed", "serde", "serde_json", "sysinfo", "thiserror 2.0.16", "tokio", - "tokio-tungstenite", "toml", + "tower", + "tower-http", "tracing", "tracing-appender", "tracing-subscriber", "tract-onnx", "url", + "uuid", "xsk-rs", ] @@ -1721,6 +1552,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -1806,7 +1643,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1908,6 +1744,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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-complex" version = "0.4.6" @@ -1919,9 +1765,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1999,6 +1845,28 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "ort" version = "2.0.0-rc.12" @@ -2053,10 +1921,15 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "password-hash" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] [[package]] name = "pastey" @@ -2064,6 +1937,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2250,6 +2133,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -2366,18 +2255,40 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-embed" version = "8.7.2" @@ -2542,6 +2453,17 @@ dependencies = [ "serde", ] +[[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_spanned" version = "1.0.4" @@ -2609,6 +2531,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.16", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -2624,16 +2558,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -2673,6 +2597,12 @@ dependencies = [ "serde", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -2695,6 +2625,12 @@ dependencies = [ "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.1" @@ -2783,30 +2719,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2849,7 +2785,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.61.1", ] @@ -2867,9 +2803,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", @@ -2877,21 +2813,6 @@ dependencies = [ "tungstenite", ] -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "hashbrown 0.14.5", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "1.0.3+spec-1.1.0" @@ -2931,6 +2852,48 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[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.41" @@ -3160,19 +3123,18 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.2", "sha1", "thiserror 2.0.16", - "utf-8", ] [[package]] @@ -3220,6 +3182,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -3232,12 +3200,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf16_iter" version = "1.0.5" @@ -3250,12 +3212,29 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "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" @@ -3284,7 +3263,25 @@ version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.45.0", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[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 0.51.0", ] [[package]] @@ -3332,6 +3329,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.2", + "indexmap", + "semver", +] + [[package]] name = "which" version = "8.0.0" @@ -3593,6 +3624,100 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +[[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" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[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", +] + +[[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", + "prettyplease", + "syn 2.0.98", + "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 2.0.98", + "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", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[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", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" @@ -3735,31 +3860,3 @@ dependencies = [ "quote", "syn 2.0.98", ] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/README.md b/README.md index 0494232..eb07164 100644 --- a/README.md +++ b/README.md @@ -10,25 +10,39 @@ - **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection - **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards -## Functional Modules +[//]: # (## Functional Modules) -### Resource Overview -![Home](.github/images/index.png) -- Real-time control system occupancy rate +[//]: # () +[//]: # (### Resource Overview) -### Dashboard Overview -![Dashboard](.github/images/dashboard.png) -- Real-time network traffic monitoring and visualization -- Recent traffic statistics and trend analysis +[//]: # (![Home](.github/images/index.png)) -### Detailed Traffic Statistics -![Statistics](.github/images/statistics.png) -- Detailed traffic usage information per IP address +[//]: # (- Real-time control system occupancy rate) -### Network Access Control -![accessControl](.github/images/accessControl.png) -- IPv4/IPv6 whitelist and blacklist management -- Precise port-level access control +[//]: # () +[//]: # (### Dashboard Overview) + +[//]: # (![Dashboard](.github/images/dashboard.png)) + +[//]: # (- Real-time network traffic monitoring and visualization) + +[//]: # (- Recent traffic statistics and trend analysis) + +[//]: # () +[//]: # (### Detailed Traffic Statistics) + +[//]: # (![Statistics](.github/images/statistics.png)) + +[//]: # (- Detailed traffic usage information per IP address) + +[//]: # () +[//]: # (### Network Access Control) + +[//]: # (![accessControl](.github/images/accessControl.png)) + +[//]: # (- IPv4/IPv6 whitelist and blacklist management) + +[//]: # (- Precise port-level access control) [//]: # (### AI Attack Detection) diff --git a/TODO b/TODO index 0182e7b..54e0c21 100644 --- a/TODO +++ b/TODO @@ -4,6 +4,7 @@ Mantis TODO (updated 2026-05-21) -- DONE (archived) ---------------------------------------- + [x] CSV rolling log with date-based filenames [x] Migrate inference engine to ort-tract (pure Rust) [x] Improve ML inference throughput under high load diff --git a/common/src/model/event.rs b/common/src/model/event.rs index e862aa4..5bd008d 100644 --- a/common/src/model/event.rs +++ b/common/src/model/event.rs @@ -109,7 +109,6 @@ impl Event { } } - #[repr(C, align(8))] #[derive(Debug, Clone)] pub struct IPv4Event { diff --git a/config.toml b/config.toml index 44186ae..6f63005 100644 --- a/config.toml +++ b/config.toml @@ -40,6 +40,13 @@ ml_cpu = 7 ae_threshold_method = "94" +# Auth system. Remove this entire section to disable auth. +[Config.auth] +jwt_secret = "change-me-jwt-secret-must-be-32-bytes-min" +db_key = "change-me-db-key-must-be-32-bytes-min-x" +token_ttl_secs = 86400 +default_admin_password = "admin" + # Suricata rule engine. Remove this entire section to disable. [Config.suricata] home_net = "140.130.34.0/24" diff --git a/lib/ebpf/info.txt b/lib/ebpf/info.txt new file mode 100644 index 0000000..ba6a1d3 --- /dev/null +++ b/lib/ebpf/info.txt @@ -0,0 +1 @@ +// this's ebpf folder \ No newline at end of file diff --git a/onnxruntime/info.txt b/lib/onnxruntime/info.txt similarity index 100% rename from onnxruntime/info.txt rename to lib/onnxruntime/info.txt diff --git a/macros/src/log.rs b/macros/src/log.rs index ff56b3f..06a698f 100644 --- a/macros/src/log.rs +++ b/macros/src/log.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{parse_macro_input, Expr, Token}; +use syn::{Expr, Token, parse_macro_input}; struct LogInput { error: Expr, @@ -62,5 +62,5 @@ pub fn log_impl(input: TokenStream) -> TokenStream { } } } - .into() + .into() } diff --git a/mantis-frontend b/mantis-frontend index f1fa091..66ae9de 160000 --- a/mantis-frontend +++ b/mantis-frontend @@ -1 +1 @@ -Subproject commit f1fa091f4c1abc90049b32c36f2d339400090713 +Subproject commit 66ae9de967702ef02ab47baa9597ab8f16e3ab91 diff --git a/mantis/Cargo.toml b/mantis/Cargo.toml index 3b965f5..d711b11 100644 --- a/mantis/Cargo.toml +++ b/mantis/Cargo.toml @@ -7,10 +7,9 @@ edition = "2024" common = { path = "../common", features = ["user"] } macros = { path = "../macros" } -actix = "0.13.5" -actix-cors = "0.7.1" -actix-web = "4.11.0" -actix-ws = "0.4.0" +axum = { version = "0.8", features = ["ws", "macros"] } +tower = { version = "0.5", features = ["util"] } +tower-http = { version = "0.6", features = ["cors"] } aya = { workspace = true } aya-log = { workspace = true } network-types = { workspace = true } @@ -25,7 +24,6 @@ serde_json = "1.0.143" sysinfo = "0.38.2" thiserror = "2.0.3" tokio = { version = "1.40.0", features = ["full", "macros"] } -tokio-tungstenite = "0.28.0" toml = "1.0.3" tracing = "0.1.41" tracing-appender = "0.2.3" @@ -37,6 +35,10 @@ lru = "0.16.2" futures = "0.3.31" tract-onnx = "0.22.1" chrono = "0.4" +rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] } +argon2 = "0.5" +jsonwebtoken = "9" +uuid = { version = "1", features = ["v4"] } ort-tract = { version = "0.3.0+0.22", optional = true } ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray"] } diff --git a/mantis/build.rs b/mantis/build.rs index 6a15d0f..e61bcf4 100644 --- a/mantis/build.rs +++ b/mantis/build.rs @@ -8,12 +8,18 @@ use std::time::SystemTime; use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target}; fn main() { + let suricata_eve_socket = PathBuf::from("/").join("tmp").join("suricata-alerts.sock"); + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let artifact_dir = manifest_dir.join("static").join("artifacts"); let rule_dir = manifest_dir.join("static").join("rules"); - let suricata_eve_socket = PathBuf::from("/tmp").join("suricata-alerts.sock"); + let db_dir = manifest_dir.join("static").join("db"); let csv_dir = manifest_dir.parent().unwrap().join("records"); - let onnxruntime_dir = manifest_dir.parent().unwrap().join("onnxruntime").join("libonnxruntime.so"); + + let lib_dir = manifest_dir.parent().unwrap().join("lib"); + let onnxruntime_dir = lib_dir.join("onnxruntime").join("libonnxruntime.so"); + let ingress_edpf_dir = lib_dir.join("ebpf").join("mantis-ingress"); + let egress_edpf_dir = lib_dir.join("ebpf").join("mantis-egress"); let static_web = manifest_dir.join("static").join("web"); let project_name = manifest_dir.file_name().unwrap().to_string_lossy().into_owned(); @@ -25,7 +31,10 @@ fn main() { println!("cargo:rustc-env=ARTIFACTCS_PATH={}", artifact_dir.display()); println!("cargo:rustc-env=CSV_RECORD_PATH={}", csv_dir.display()); println!("cargo:rustc-env=ONNXRUNTIME_PATH={}", onnxruntime_dir.display()); + println!("cargo:rustc-env=INGRESS_PATH={}", ingress_edpf_dir.display()); + println!("cargo:rustc-env=EGRESS_PATH={}", egress_edpf_dir.display()); println!("cargo:rustc-env=RULE_PATH={}", rule_dir.display()); + println!("cargo:rustc-env=DB_PATH={}", db_dir.display()); println!("cargo:rustc-env=RULE_EVE_PATH={}", suricata_eve_socket.display()); for item in &[ @@ -42,21 +51,19 @@ fn main() { } if env::var_os("SKIP_EBPF_BUILD").is_some() { - let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - for name in &["mantis-ingress", "mantis-egress"] { - let path = out.join(name); + for path in &[&ingress_edpf_dir, &egress_edpf_dir] { if !path.exists() { - fs::write(&path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}")); + fs::write(path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}")); } } return; } - build_ingress_ebpf(); - build_egress_ebpf(); + build_ingress_ebpf(&ingress_edpf_dir); + build_egress_ebpf(&egress_edpf_dir); build_frontend(&frontend_dir, &static_web); } -fn build_ingress_ebpf() { +fn build_ingress_ebpf(dst: &PathBuf) { let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); let ebpf_package = packages .into_iter() @@ -155,14 +162,12 @@ fn build_ingress_ebpf() { stderr.join().map_err(std::panic::resume_unwind).unwrap(); - for (name, binary) in executables { - let dst = out_dir.join(name); - let _: u64 = - fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + for (_name, binary) in executables { + let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); } } -fn build_egress_ebpf() { +fn build_egress_ebpf(dst: &PathBuf) { let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); let ebpf_package = packages .into_iter() @@ -262,10 +267,8 @@ fn build_egress_ebpf() { stderr.join().map_err(std::panic::resume_unwind).unwrap(); - for (name, binary) in executables { - let dst = out_dir.join(name); - let _: u64 = - fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + for (_name, binary) in executables { + let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); } } diff --git a/mantis/src/core/app_state.rs b/mantis/src/core/app_state.rs new file mode 100644 index 0000000..d29be2a --- /dev/null +++ b/mantis/src/core/app_state.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use crate::core::ebpf::access_control::AccessControl; +use crate::core::ebpf::service::Service; +use crate::core::ebpf::statistics::Statistics; +use crate::core::infrastructure::app_config::AppConfig; +use crate::core::infrastructure::app_db::AppDB; +use crate::core::infrastructure::detection_alert::DetectionAlert; +use crate::core::infrastructure::health::SystemHealth; +use crate::detection::ml::config_loader::InferenceConfig; + +#[derive(Clone)] +pub struct AppState { + pub app_config: Arc, + pub inference_config: Arc, + pub access_control: Arc, + pub service: Arc, + pub statistics: Arc, + pub health: Arc, + pub detection_alert: Arc, + pub app_db: Option>, +} diff --git a/mantis/src/core/ebpf/access_control.rs b/mantis/src/core/ebpf/access_control.rs index 73df7d1..38b0206 100644 --- a/mantis/src/core/ebpf/access_control.rs +++ b/mantis/src/core/ebpf/access_control.rs @@ -8,8 +8,8 @@ use common::model::ip_address::{IPv4, IPv6, Port}; use tokio::sync::RwLock; use crate::model::direction::FlowDirection; -use crate::model::error::ebpf::EbpfError; use crate::model::error::Error; +use crate::model::error::ebpf::EbpfError; use crate::model::ip_address::NativeConvert; use crate::model::list_type::ListType; use crate::utils::ip_address::convert_ports_to_vec; diff --git a/mantis/src/core/ebpf/mod.rs b/mantis/src/core/ebpf/mod.rs index f3bccaa..b69e43e 100644 --- a/mantis/src/core/ebpf/mod.rs +++ b/mantis/src/core/ebpf/mod.rs @@ -17,8 +17,8 @@ use crate::core::ebpf::xsk_manager::XskManager; use crate::core::infrastructure::app_config::AppConfig; use crate::detection::ml::engine::Engine; use crate::detection::suricata::SuricataEngine; -use crate::model::error::system::SystemError; use crate::model::error::Error; +use crate::model::error::system::SystemError; pub struct EbpfServices { pub xsk_manager: Arc, @@ -29,11 +29,7 @@ pub struct EbpfServices { } impl EbpfServices { - pub fn new( - app_config: Arc, - ingress_ebpf: &mut Ebpf, - egress_ebpf: &mut Ebpf, - ) -> Result { + pub fn new(app_config: Arc, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result { let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?; let access_control = AccessControl::new(ingress_ebpf)?; let service = Service::new(ingress_ebpf)?; @@ -70,4 +66,4 @@ impl EbpfServices { } } } -} \ No newline at end of file +} diff --git a/mantis/src/core/ebpf/service.rs b/mantis/src/core/ebpf/service.rs index 7afe782..0fe8568 100644 --- a/mantis/src/core/ebpf/service.rs +++ b/mantis/src/core/ebpf/service.rs @@ -8,8 +8,8 @@ use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6}; use common::model::placeholder::PlaceHolder; use tokio::sync::RwLock; -use crate::model::error::ebpf::EbpfError; use crate::model::error::Error; +use crate::model::error::ebpf::EbpfError; use crate::model::ip_address::NativeConvert; pub struct Service { diff --git a/mantis/src/core/ebpf/statistics.rs b/mantis/src/core/ebpf/statistics.rs index 8d37afd..f783d55 100644 --- a/mantis/src/core/ebpf/statistics.rs +++ b/mantis/src/core/ebpf/statistics.rs @@ -160,11 +160,13 @@ impl Statistics { let map = self .ipv4_maps .get(&(direction, flow_direction, time_type)) - .ok_or_else(|| EbpfError::FlowMapKeyMissing( - format!("{:?}", direction), - format!("{:?}", flow_direction), - format!("{:?}", time_type), - ))?; + .ok_or_else(|| { + EbpfError::FlowMapKeyMissing( + format!("{:?}", direction), + format!("{:?}", flow_direction), + format!("{:?}", time_type), + ) + })?; let flow_data = map.write().await.get_map(); if let Some(ref geo_ip) = self.geo_ip { @@ -197,11 +199,13 @@ impl Statistics { let map = self .ipv6_maps .get(&(direction, flow_direction, time_type)) - .ok_or_else(|| EbpfError::FlowMapKeyMissing( - format!("{:?}", direction), - format!("{:?}", flow_direction), - format!("{:?}", time_type), - ))?; + .ok_or_else(|| { + EbpfError::FlowMapKeyMissing( + format!("{:?}", direction), + format!("{:?}", flow_direction), + format!("{:?}", time_type), + ) + })?; let flow_data = map.write().await.get_map(); if let Some(ref geo_ip) = self.geo_ip { diff --git a/mantis/src/core/ebpf/xsk_manager.rs b/mantis/src/core/ebpf/xsk_manager.rs index 3f67667..39bbb7f 100644 --- a/mantis/src/core/ebpf/xsk_manager.rs +++ b/mantis/src/core/ebpf/xsk_manager.rs @@ -6,9 +6,9 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -use aya::maps::{MapData, XskMap}; use aya::Ebpf; -use crossbeam::channel::{bounded, Receiver, Sender}; +use aya::maps::{MapData, XskMap}; +use crossbeam::channel::{Receiver, Sender, bounded}; use crossbeam::queue::SegQueue; use macros::log; use parking_lot::Mutex; @@ -21,9 +21,9 @@ use crate::detection::ml::engine::Engine; use crate::detection::suricata::SuricataEngine; use crate::model::config::Config; use crate::model::direction::Direction; +use crate::model::error::Error; use crate::model::error::ebpf::EbpfError; use crate::model::error::system::SystemError; -use crate::model::error::Error; use crate::model::log::ebpf::EbpfLog; use crate::utils::cpu_affinity::set_cpu_affinity; @@ -210,7 +210,6 @@ impl XskPair { set_cpu_affinity((start + self.queue_id % num_cores) as usize); } - let mut shutdown_rx = Some(shutdown_rx); let mut idle_count: u32 = 0; let mut last_cleanup = std::time::Instant::now(); @@ -286,10 +285,7 @@ impl XskPair { Ok(nb_completed) } - fn process_rx_queue( - &mut self, - forward_tx: &Sender>, - ) -> Result { + fn process_rx_queue(&mut self, forward_tx: &Sender>) -> Result { let mut rx_descs = vec![FrameDesc::default(); 64]; let rx_count = unsafe { self.rx.consume(&mut rx_descs) }; @@ -394,4 +390,4 @@ impl XskPair { Ok(nb_submitted) } -} \ No newline at end of file +} diff --git a/mantis/src/core/infrastructure/app_config.rs b/mantis/src/core/infrastructure/app_config.rs index 7f67ee0..a01e003 100644 --- a/mantis/src/core/infrastructure/app_config.rs +++ b/mantis/src/core/infrastructure/app_config.rs @@ -2,8 +2,8 @@ use std::fs; use std::ops::Deref; use crate::model::config::{Config, ConfigTable}; -use crate::model::error::system::SystemError; use crate::model::error::Error; +use crate::model::error::system::SystemError; pub struct AppConfig { pub config: Config, diff --git a/mantis/src/core/infrastructure/app_db.rs b/mantis/src/core/infrastructure/app_db.rs new file mode 100644 index 0000000..41dedb1 --- /dev/null +++ b/mantis/src/core/infrastructure/app_db.rs @@ -0,0 +1,115 @@ +use std::path::Path; +use std::sync::Mutex; + +use argon2::Argon2; +use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}; +use macros::log; +use rusqlite::{Connection, params}; + +use crate::model::error::Error; +use crate::model::error::auth::AuthError; +use crate::model::log::auth::AuthLog; + +pub struct Account { + pub id: String, + pub username: String, + pub password_hash: String, + pub role: String, +} + +pub struct AppDB { + conn: Mutex, +} + +impl AppDB { + pub fn open(path: impl AsRef, key: &str) -> Result { + let path = path.as_ref(); + + if let Some(parent) = Path::new(path).parent() { + std::fs::create_dir_all(parent).map_err(|e| AuthError::DBError { msg: e.to_string() })?; + } + + let conn = Connection::open(path).map_err(|e| AuthError::DBError { msg: e.to_string() })?; + + // Must be the first statement on the connection to unlock the encrypted DB. + conn.execute_batch(&format!("PRAGMA key = '{}';", key.replace('\'', "''"))) + .map_err(|e| AuthError::DBError { msg: e.to_string() })?; + + conn.execute_batch( + "PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS accounts ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'viewer', + created_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + account_id TEXT NOT NULL, + expires_at INTEGER NOT NULL, + FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE + );", + ) + .map_err(|e| AuthError::DBError { msg: e.to_string() })?; + + log!(AuthLog::DbInitialized { + path: path.to_string_lossy().to_string() + }); + Ok(Self { conn: Mutex::new(conn) }) + } + + pub fn ensure_default_admin(&self, default_password: &str) -> Result<(), Error> { + let conn = self.conn.lock().unwrap(); + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0)) + .map_err(|e| AuthError::DBError { msg: e.to_string() })?; + + if count == 0 { + let hash = hash_password(default_password)?; + let now = chrono::Utc::now().timestamp(); + conn.execute( + "INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + params!["admin", "admin", hash, "admin", now], + ) + .map_err(|e| AuthError::DBError { msg: e.to_string() })?; + log!(AuthLog::DefaultAdminCreated); + } + Ok(()) + } + + pub fn find_account_by_username(&self, username: &str) -> Result, Error> { + let conn = self.conn.lock().unwrap(); + let result = conn.query_row( + "SELECT id, username, password_hash, role FROM accounts WHERE username = ?1", + params![username], + |row| { + Ok(Account { + id: row.get(0)?, + username: row.get(1)?, + password_hash: row.get(2)?, + role: row.get(3)?, + }) + }, + ); + match result { + Ok(account) => Ok(Some(account)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(AuthError::DBError { msg: e.to_string() }.into()), + } + } +} + +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + Argon2::default() + .hash_password(password.as_bytes(), &salt) + .map(|h| h.to_string()) + .map_err(|_| AuthError::HashError.into()) +} + +pub fn verify_password(password: &str, stored_hash: &str) -> bool { + PasswordHash::new(stored_hash) + .map(|h| Argon2::default().verify_password(password.as_bytes(), &h).is_ok()) + .unwrap_or(false) +} diff --git a/mantis/src/core/infrastructure/geoip.rs b/mantis/src/core/infrastructure/geoip.rs index 1a59d41..8ce73d4 100644 --- a/mantis/src/core/infrastructure/geoip.rs +++ b/mantis/src/core/infrastructure/geoip.rs @@ -1,11 +1,11 @@ use std::net::IpAddr; +use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::Arc; -use maxminddb::{geoip2, MaxMindDbError, Reader}; -use tokio::sync::{RwLock, Semaphore}; use lru::LruCache; -use std::num::NonZeroUsize; +use maxminddb::{MaxMindDbError, Reader, geoip2}; +use tokio::sync::{RwLock, Semaphore}; use tokio::task; use crate::model::geo_stats::GeoLocation; @@ -25,13 +25,10 @@ impl GeoIpService { Self::with_cache_size(db_path, 10000) } - pub fn with_cache_size>( - db_path: P, - cache_size: usize, - ) -> Result { + pub fn with_cache_size>(db_path: P, cache_size: usize) -> Result { let reader = Reader::open_readfile(db_path)?; - let cache_capacity = NonZeroUsize::new(cache_size) - .unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero")); + let cache_capacity = + NonZeroUsize::new(cache_size).unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero")); Ok(Self { reader: Arc::new(reader), @@ -59,7 +56,11 @@ impl GeoIpService { } } - let permit = self.lookup_sem.clone().acquire_owned().await + let permit = self + .lookup_sem + .clone() + .acquire_owned() + .await .map_err(|_| MaxMindDbError::InvalidDatabase { message: "GeoIP semaphore closed".to_string(), offset: None, @@ -70,11 +71,11 @@ impl GeoIpService { let _permit = permit; Self::lookup_from_db_blocking(&reader, ip) }) - .await - .map_err(|e| MaxMindDbError::InvalidDatabase { - message: format!("Task join error: {}", e), - offset: None, - })??; + .await + .map_err(|e| MaxMindDbError::InvalidDatabase { + message: format!("Task join error: {}", e), + offset: None, + })??; { let mut cache = self.cache.write().await; @@ -84,22 +85,16 @@ impl GeoIpService { Ok(result) } - fn lookup_from_db_blocking( - reader: &Reader>, - ip: IpAddr, - ) -> Result, MaxMindDbError> { + fn lookup_from_db_blocking(reader: &Reader>, ip: IpAddr) -> Result, MaxMindDbError> { let lookup_result = reader.lookup(ip)?; let city_option: Option = lookup_result.decode()?; Ok(city_option.map(|city| { - let country_name = city.country.names.english - .map(|s| s.to_string()); + let country_name = city.country.names.english.map(|s| s.to_string()); - let country_code = city.country.iso_code - .map(|s| s.to_string()); + let country_code = city.country.iso_code.map(|s| s.to_string()); - let city_name = city.city.names.english - .map(|s| s.to_string()); + let city_name = city.city.names.english.map(|s| s.to_string()); let latitude = city.location.latitude.or(Some(0.0)); let longitude = city.location.longitude.or(Some(0.0)); @@ -120,4 +115,4 @@ impl GeoIpService { let cache = self.cache.read().await; (cache.len(), cache.cap().get()) } -} \ No newline at end of file +} diff --git a/mantis/src/core/infrastructure/health.rs b/mantis/src/core/infrastructure/health.rs index ce4b33d..3b31373 100644 --- a/mantis/src/core/infrastructure/health.rs +++ b/mantis/src/core/infrastructure/health.rs @@ -1,25 +1,18 @@ use std::sync::Arc; use std::time::Duration; -use sysinfo::{Components, Networks, System}; -use tokio::sync::{broadcast, oneshot, RwLock}; -use tokio::time::interval; use macros::log; +use sysinfo::{Components, Networks, System}; +use tokio::sync::{RwLock, broadcast, oneshot}; +use tokio::time::interval; use crate::core::infrastructure::app_config::AppConfig; -use crate::model::log::health::Health; use crate::model::error::Error; use crate::model::health::{ - ConfiguredNetworkStats, - CpuCoreInfo, - CpuDetails, - LoadAverage, - MemoryUsage, - NetworkStats, - SystemHealthMetrics, - SystemHealthStatus, - SystemInfo + ConfiguredNetworkStats, CpuCoreInfo, CpuDetails, LoadAverage, MemoryUsage, NetworkStats, SystemHealthMetrics, + SystemHealthStatus, SystemInfo, }; +use crate::model::log::health::Health; pub struct SystemHealth { system: RwLock, @@ -30,7 +23,6 @@ pub struct SystemHealth { egress_interface: String, } - impl SystemHealth { pub fn new(config: Arc) -> Result { let (broadcast_tx, _) = broadcast::channel(100); @@ -125,11 +117,7 @@ impl SystemHealth { swap_used: system.used_swap(), }; - let network_stats = Self::collect_configured_network_stats( - networks, - ingress_interface, - egress_interface, - ); + let network_stats = Self::collect_configured_network_stats(networks, ingress_interface, egress_interface); let load_average = System::load_average(); let load_average = if load_average.one != 0.0 || load_average.five != 0.0 || load_average.fifteen != 0.0 { @@ -229,16 +217,19 @@ impl SystemHealth { let egress = create_network_stats(egress_interface); if ingress.is_none() { - log!(Health::InterfaceNotFound("Ingress".to_string(), ingress_interface.to_string())); + log!(Health::InterfaceNotFound( + "Ingress".to_string(), + ingress_interface.to_string() + )); } if egress.is_none() { - log!(Health::InterfaceNotFound("Egress".to_string(), egress_interface.to_string())); + log!(Health::InterfaceNotFound( + "Egress".to_string(), + egress_interface.to_string() + )); } - ConfiguredNetworkStats { - ingress, - egress, - } + ConfiguredNetworkStats { ingress, egress } } pub async fn get_current_metrics(&self) -> SystemHealthMetrics { @@ -290,22 +281,17 @@ impl SystemHealth { metrics.memory_usage.usage_percent )); } else if metrics.memory_usage.usage_percent > 80.0 { - status.warnings.push(format!( - "High memory usage: {:.1}%", - metrics.memory_usage.usage_percent - )); + status + .warnings + .push(format!("High memory usage: {:.1}%", metrics.memory_usage.usage_percent)); } if let Some(temp) = metrics.temperature { if temp > 80.0 { status.overall_healthy = false; - status - .issues - .push(format!("High CPU temperature: {:.1}°C", temp)); + status.issues.push(format!("High CPU temperature: {:.1}°C", temp)); } else if temp > 70.0 { - status - .warnings - .push(format!("Elevated CPU temperature: {:.1}°C", temp)); + status.warnings.push(format!("Elevated CPU temperature: {:.1}°C", temp)); } } @@ -320,4 +306,4 @@ impl SystemHealth { status } -} \ No newline at end of file +} diff --git a/mantis/src/core/infrastructure/mod.rs b/mantis/src/core/infrastructure/mod.rs index eea9b0a..4227c78 100644 --- a/mantis/src/core/infrastructure/mod.rs +++ b/mantis/src/core/infrastructure/mod.rs @@ -1,7 +1,8 @@ pub mod app_config; +pub mod app_db; pub mod detection_alert; -pub mod health; pub mod geoip; +pub mod health; use std::path::PathBuf; use std::sync::Arc; @@ -13,6 +14,7 @@ use macros::log; use tokio::sync::oneshot; use crate::core::infrastructure::app_config::AppConfig; +use crate::core::infrastructure::app_db::AppDB; use crate::core::infrastructure::detection_alert::DetectionAlert; use crate::core::infrastructure::health::SystemHealth; use crate::detection::fusion::{FusionEngine, FusionMode}; @@ -22,8 +24,8 @@ use crate::detection::ml::feature_extractor::FlowFeatures; use crate::detection::ml::model_loader::MLModels; use crate::detection::ml::traffic_logger::TrafficLogger; use crate::detection::suricata::SuricataEngine; -use crate::model::error::misc::MiscError; use crate::model::error::Error; +use crate::model::error::misc::MiscError; use crate::model::error::system::SystemError; use crate::model::log::system::SystemLog; @@ -34,6 +36,7 @@ pub struct AppServices { pub ml_models: Arc, pub ml_engine: Arc, pub suricata_engine: Option>, + pub app_db: Option>, shutdowns: SegQueue>, } @@ -53,12 +56,21 @@ impl AppServices { let traffic_logger = if app_config.traffic_logging_mode { let dir = PathBuf::from(env!("CSV_RECORD_PATH")); let basename = app_config.traffic_log_csv_path.trim_end_matches(".csv").to_string(); - let mut header = vec!["Source IP".to_string(), "Destination IP".to_string(), "Timestamp".to_string()]; + let mut header = vec![ + "Source IP".to_string(), + "Destination IP".to_string(), + "Timestamp".to_string(), + ]; header.extend(FlowFeatures::all_feature_names_owned()); header.push("Label".to_string()); let logger = TrafficLogger::new(&dir, &basename, header) .map_err(|e| MiscError::TrafficLogCreateError(dir.display().to_string(), e.to_string()))?; - log!(SystemLog::TrafficLoggingEnabled(format!("{}/{}-{}.csv", dir.display(), basename, Local::now().format("%Y-%m-%d")))); + log!(SystemLog::TrafficLoggingEnabled(format!( + "{}/{}-{}.csv", + dir.display(), + basename, + Local::now().format("%Y-%m-%d") + ))); Some(Arc::new(logger)) } else { None @@ -80,7 +92,21 @@ impl AppServices { let suricata_engine = if let Some(ref sc) = app_config.suricata { let rule_path = PathBuf::from(env!("RULE_PATH")); let eve_socket = PathBuf::from(env!("RULE_EVE_PATH")); - Some(SuricataEngine::start(sc, &rule_path, &eve_socket, fusion_engine.clone())?) + Some(SuricataEngine::start( + sc, + &rule_path, + &eve_socket, + fusion_engine.clone(), + )?) + } else { + None + }; + + let app_db = if let Some(ref auth) = app_config.auth { + let db_path = PathBuf::from(env!("DB_PATH")).join("app.db"); + let db = AppDB::open(db_path, &auth.db_key)?; + db.ensure_default_admin(&auth.default_admin_password)?; + Some(Arc::new(db)) } else { None }; @@ -92,6 +118,7 @@ impl AppServices { ml_models, ml_engine, suricata_engine, + app_db, shutdowns: SegQueue::new(), }) } @@ -116,4 +143,4 @@ impl AppServices { } } } -} \ No newline at end of file +} diff --git a/mantis/src/core/mod.rs b/mantis/src/core/mod.rs index 992c3fb..42ef651 100644 --- a/mantis/src/core/mod.rs +++ b/mantis/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod app_state; pub mod ebpf; pub mod infrastructure; pub mod system; diff --git a/mantis/src/core/system.rs b/mantis/src/core/system.rs index 6db03f2..b72a26c 100644 --- a/mantis/src/core/system.rs +++ b/mantis/src/core/system.rs @@ -1,26 +1,29 @@ use std::sync::Arc; -use actix_web::web::route; -use actix_web::{web, App, HttpServer}; +use axum::Router; +use aya::Ebpf; use aya::maps::{MapData, ProgramArray}; use aya::programs::{Xdp, XdpFlags}; -use aya::Ebpf; use aya_log::EbpfLogger; use common::define::program_array::*; use macros::log; +use tokio::net::TcpListener; +use tower_http::cors::CorsLayer; +use crate::core::app_state::AppState; use crate::core::ebpf::EbpfServices; -use crate::core::infrastructure::app_config::AppConfig; use crate::core::infrastructure::AppServices; +use crate::core::infrastructure::app_config::AppConfig; use crate::detection::ml::config_loader::InferenceConfig; +use crate::model::error::Error; use crate::model::error::ebpf::EbpfError; use crate::model::error::http::HttpError; use crate::model::error::misc::MiscError; -use crate::model::error::Error; use crate::model::log::ml::MLLog; use crate::model::log::system::SystemLog; use crate::utils::logging::Logging; -use crate::web::api::{control, default, detection_alert, health, misc}; +use crate::web::api::default::default_route; +use crate::web::api::{auth, control, detection_alert, health, misc}; pub struct System { pub app_config: Arc, @@ -41,7 +44,10 @@ impl System { let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?; let app_config = Arc::new(AppConfig::new()?); - let inference_config = Arc::new(InferenceConfig::load_file(&app_config.models_config_name, &app_config.ae_threshold_method)?); + let inference_config = Arc::new(InferenceConfig::load_file( + &app_config.models_config_name, + &app_config.ae_threshold_method, + )?); let ebpf_services = Arc::new(EbpfServices::new( app_config.clone(), @@ -82,7 +88,9 @@ impl System { log!(SystemLog::InitializeComplete); self.attach_ebpf()?; - ebpf_services.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone()).await?; + ebpf_services + .run(app_services.ml_engine.clone(), app_services.suricata_engine.clone()) + .await?; app_services.run().await?; self.run_http_server().await?; Ok(()) @@ -134,49 +142,45 @@ impl System { } async fn run_http_server(&self) -> Result<(), Error> { - let app_config = self.app_config.clone(); - let inference_config = self.inference_config.clone(); - let access_control = self.ebpf_services.access_control.clone(); - let service = self.ebpf_services.service.clone(); - let statistics = self.ebpf_services.statistics.clone(); - let health = self.app_services.health.clone(); - let detection_alert = self.app_services.detection_alert.clone(); + let state = AppState { + app_config: self.app_config.clone(), + inference_config: self.inference_config.clone(), + access_control: self.ebpf_services.access_control.clone(), + service: self.ebpf_services.service.clone(), + statistics: self.ebpf_services.statistics.clone(), + health: self.app_services.health.clone(), + detection_alert: self.app_services.detection_alert.clone(), + app_db: self.app_services.app_db.clone(), + }; + + let app = Router::new() + .nest("/ebpf", control::router()) + .nest("/detection", detection_alert::router()) + .nest("/health", health::router()) + .nest("/misc", misc::router()) + .nest("/auth", auth::router()) + .fallback(default_route) + .layer(CorsLayer::permissive()) + .with_state(state); + let port = self.app_config.http_server_bind_port; - HttpServer::new(move || { - let cors = actix_cors::Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .max_age(3600); - App::new() - .wrap(cors) - .app_data(web::Data::from(app_config.clone())) - .app_data(web::Data::from(inference_config.clone())) - .app_data(web::Data::from(access_control.clone())) - .app_data(web::Data::from(service.clone())) - .app_data(web::Data::from(statistics.clone())) - .app_data(web::Data::from(health.clone())) - .app_data(web::Data::from(detection_alert.clone())) - .service(control::initialize()) - .service(detection_alert::initialize()) - .service(health::initialize()) - .service(misc::initialize()) - .default_service(route().to(default::default_route)) - }) - .bind(format!("0.0.0.0:{}", port)) - .map_err(HttpError::BindPortError)? - .run() - .await - .map_err(HttpError::ServerPanic)?; + let listener = TcpListener::bind(format!("0.0.0.0:{}", port)) + .await + .map_err(HttpError::BindPortError)?; + + axum::serve(listener, app) + .with_graceful_shutdown(async { + tokio::signal::ctrl_c().await.ok(); + }) + .await + .map_err(HttpError::ServerPanic)?; + Ok(()) } fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray), Error> { - let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( - env!("OUT_DIR"), - "/mantis-ingress" - ))) - .map_err(EbpfError::EbpfNotFound)?; + let mut ingress_ebpf = + Ebpf::load(aya::include_bytes_aligned!(env!("INGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?; let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?; Self::load_program( @@ -197,11 +201,8 @@ impl System { } fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray), Error> { - let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( - env!("OUT_DIR"), - "/mantis-egress" - ))) - .map_err(EbpfError::EbpfNotFound)?; + let mut egress_ebpf = + Ebpf::load(aya::include_bytes_aligned!(env!("EGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?; let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?; Self::load_program(&mut egress_ebpf, &mut program_array, "statistics", egress::STATISTICS)?; diff --git a/mantis/src/detection/fusion.rs b/mantis/src/detection/fusion.rs index b88b988..13e84a1 100644 --- a/mantis/src/detection/fusion.rs +++ b/mantis/src/detection/fusion.rs @@ -14,7 +14,11 @@ pub enum FusionMode { impl FusionMode { pub fn from_str(s: &str) -> Self { - if s.eq_ignore_ascii_case("and") { FusionMode::And } else { FusionMode::Or } + if s.eq_ignore_ascii_case("and") { + FusionMode::And + } else { + FusionMode::Or + } } } @@ -60,7 +64,14 @@ impl FusionEngine { return; } self.alert.broadcast(UnifiedAlert::from_ml(result)); - map.insert(key, FusionState { ml: Some(result.clone()), rule: None, created_at: now }); + map.insert( + key, + FusionState { + ml: Some(result.clone()), + rule: None, + created_at: now, + }, + ); } FusionMode::And => { let maybe_rule = { @@ -100,7 +111,14 @@ impl FusionEngine { return; } self.alert.broadcast(UnifiedAlert::from_rule(m)); - map.insert(key, FusionState { ml: None, rule: Some(m.clone()), created_at: now }); + map.insert( + key, + FusionState { + ml: None, + rule: Some(m.clone()), + created_at: now, + }, + ); } FusionMode::And => { let maybe_ml = { diff --git a/mantis/src/detection/ml/aggregator.rs b/mantis/src/detection/ml/aggregator.rs index d1ee27b..cadccbe 100644 --- a/mantis/src/detection/ml/aggregator.rs +++ b/mantis/src/detection/ml/aggregator.rs @@ -5,7 +5,7 @@ use crate::model::ml_detection::FlowKey; // L2 thresholds: anomalous flows per src_ip within the aggregation window const FLOOD_THRESHOLD: usize = 10; // total anomalous flows -> FLOOD -const SCAN_THRESHOLD: usize = 5; // distinct dst_ports -> SCAN +const SCAN_THRESHOLD: usize = 5; // distinct dst_ports -> SCAN struct SrcIpState { events: Vec<(Instant, u16)>, // (time, dst_port) @@ -39,8 +39,7 @@ impl AttackAggregator { detections.push((now, score)); if detections.len() >= self.min_detections { - let avg_score: f32 = - detections.iter().map(|(_, s)| s).sum::() / detections.len() as f32; + let avg_score: f32 = detections.iter().map(|(_, s)| s).sum::() / detections.len() as f32; return avg_score > threshold * self.alert_threshold_multiplier; } @@ -52,10 +51,13 @@ impl AttackAggregator { let now = Instant::now(); let window = self.window_duration; - let state = self.src_ip_states.entry(src_ip.to_string()).or_insert_with(|| SrcIpState { - events: Vec::new(), - last_alert: None, - }); + let state = self + .src_ip_states + .entry(src_ip.to_string()) + .or_insert_with(|| SrcIpState { + events: Vec::new(), + last_alert: None, + }); state.events.retain(|(t, _)| now.duration_since(*t) < window); state.events.push((now, dst_port)); @@ -89,7 +91,9 @@ impl AttackAggregator { !detections.is_empty() }); self.src_ip_states.retain(|_, state| { - state.events.retain(|(t, _)| now.duration_since(*t) < self.window_duration); + state + .events + .retain(|(t, _)| now.duration_since(*t) < self.window_duration); !state.events.is_empty() }); } @@ -97,4 +101,4 @@ impl AttackAggregator { pub fn tracked_flows(&self) -> usize { self.detections.len() } -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/config_loader.rs b/mantis/src/detection/ml/config_loader.rs index b2e6646..57bf6a3 100644 --- a/mantis/src/detection/ml/config_loader.rs +++ b/mantis/src/detection/ml/config_loader.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; -use serde::{Deserialize, Serialize}; use macros::log; +use serde::{Deserialize, Serialize}; + use crate::model::error::ml::MLError; use crate::model::ml_detection::ClipParams; @@ -25,15 +26,18 @@ pub struct InferenceConfig { impl InferenceConfig { pub fn load_file(file: &str, method: &str) -> Result { let path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(file); - let content = fs::read_to_string(&path) - .map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?; - let mut config: InferenceConfig = serde_json::from_str(&content) - .map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?; + let content = fs::read_to_string(&path).map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?; + let mut config: InferenceConfig = + serde_json::from_str(&content).map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?; // Validate the method exists at startup — fail fast rather than at inference time. if !config.ae_thresholds.contains_key(method) { - log!(MLError::ThresholdMethodNotFound { method: method.to_string() }); - return Err(MLError::ThresholdMethodNotFound { method: method.to_string() }); + log!(MLError::ThresholdMethodNotFound { + method: method.to_string() + }); + return Err(MLError::ThresholdMethodNotFound { + method: method.to_string(), + }); } config.ae_threshold_method = method.to_string(); @@ -43,4 +47,4 @@ impl InferenceConfig { pub fn num_ae_features(&self) -> usize { self.ae_feature_names.len() } -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/engine.rs b/mantis/src/detection/ml/engine.rs index 49d9c00..1cad052 100644 --- a/mantis/src/detection/ml/engine.rs +++ b/mantis/src/detection/ml/engine.rs @@ -12,7 +12,6 @@ use super::flow_tracker::FlowTracker; use super::inference::Inference; use super::model_loader::MLModels; use super::traffic_logger::TrafficLogger; - use crate::detection::fusion::FusionEngine; use crate::model::error::ml::MLError; use crate::model::log::ml::MLLog; @@ -81,9 +80,9 @@ impl Engine { loop { tokio::select! { - _ = &mut shutdown_rx => break, - _ = ticker.tick() => {} - } + _ = &mut shutdown_rx => break, + _ = ticker.tick() => {} + } let (total_flows, flows, active_ips) = { let Ok(mut t) = self.tracker.lock() else { @@ -97,10 +96,8 @@ impl Engine { // (which is always empty). This preserves per-src_ip inference buffers // across consecutive cycles so the LSTM window can fill up over time. // A src_ip absent from this cycle loses its buffer on the next cleanup. - let active_ips: std::collections::HashSet = flows - .iter() - .map(|f| f.flow_key.src_ip.clone()) - .collect(); + let active_ips: std::collections::HashSet = + flows.iter().map(|f| f.flow_key.src_ip.clone()).collect(); (total_flows, flows, active_ips) }; @@ -122,7 +119,9 @@ impl Engine { let mut batch = flows[..flows.len().min(self.batch_size)].to_vec(); batch.sort_by(|a, b| { - a.flow_key.src_ip.cmp(&b.flow_key.src_ip) + a.flow_key + .src_ip + .cmp(&b.flow_key.src_ip) .then_with(|| a.start_time_us.cmp(&b.start_time_us)) }); @@ -131,7 +130,9 @@ impl Engine { let pipeline = Arc::clone(&self.inference_pipeline); let ml_cpu = self.ml_cpu; let mut handle = tokio::task::spawn_blocking(move || { - let cpu = ml_cpu.map(|c| c as usize).unwrap_or_else(|| num_cpus().saturating_sub(1)); + let cpu = ml_cpu + .map(|c| c as usize) + .unwrap_or_else(|| num_cpus().saturating_sub(1)); set_cpu_affinity(cpu); pipeline.infer_batch(&batch) }); @@ -166,11 +167,8 @@ impl Engine { for result in &results { if result.is_attack { // L1: full 5-tuple aggregation for persistent same-port attacks - let should_alert = aggregator.should_alert( - &result.flow_key_raw, - result.ae_score, - result.threshold, - ); + let should_alert = + aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold); if should_alert { log!(MLLog::ThreatDetected( format!("{:?}", result.direction), @@ -185,10 +183,9 @@ impl Engine { // L2: src_ip-level scan/flood detection (skipped if L1 already fired) if !alerted_src_ips.contains(&result.flow_key_raw.src_ip) { - if let Some(attack_type) = aggregator.should_alert_src_ip( - &result.flow_key_raw.src_ip, - result.flow_key_raw.dst_port, - ) { + if let Some(attack_type) = aggregator + .should_alert_src_ip(&result.flow_key_raw.src_ip, result.flow_key_raw.dst_port) + { let mut l2_result = result.clone(); l2_result.attack_type = Some(attack_type.to_string()); log!(MLLog::ThreatDetected( @@ -235,4 +232,4 @@ impl Engine { }; EngineStats { active_flows } } -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/feature_extractor.rs b/mantis/src/detection/ml/feature_extractor.rs index 3b2e236..2843c74 100644 --- a/mantis/src/detection/ml/feature_extractor.rs +++ b/mantis/src/detection/ml/feature_extractor.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; -use chrono::{Utc, TimeZone}; + +use chrono::{TimeZone, Utc}; use super::flow_tracker::FlowData; use crate::model::ml_detection::{ClipParams, PacketData}; @@ -67,7 +68,9 @@ impl FlowFeatures { let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64; let all_lengths: Vec = flow - .fwd_packets.iter().chain(flow.bwd_packets.iter()) + .fwd_packets + .iter() + .chain(flow.bwd_packets.iter()) .map(|p| p.payload_length as f64) .collect(); let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths); @@ -75,11 +78,7 @@ impl FlowFeatures { let fwd_bulk = &flow.fwd_bulk_state; let bwd_bulk = &flow.bwd_bulk_state; - let fwd_header_sizes: Vec = flow - .fwd_packets - .iter() - .map(|p| p.header_length as f64) - .collect(); + let fwd_header_sizes: Vec = flow.fwd_packets.iter().map(|p| p.header_length as f64).collect(); let (active_max, active_min, active_mean, active_std) = compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::>()); @@ -145,14 +144,22 @@ impl FlowFeatures { "Avg Fwd Segment Size" | "avg_fwd_seg_size" => safe_div(flow.fwd_total_bytes as f64, fwd_count), "Avg Bwd Segment Size" | "avg_bwd_seg_size" => safe_div(flow.bwd_total_bytes as f64, bwd_count), "Fwd Header Length.1" | "fwd_header_length_1" => flow.fwd_header_bytes as f64, - "Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64), - "Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64), + "Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => { + safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64) + } + "Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => { + safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64) + } "Fwd Avg Bulk Rate" | "fwd_avg_bulk_rate" => safe_div( fwd_bulk.total_bytes as f64, fwd_bulk.total_duration_us as f64 / 1_000_000.0, ), - "Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64), - "Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64), + "Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => { + safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64) + } + "Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => { + safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64) + } "Bwd Avg Bulk Rate" | "bwd_avg_bulk_rate" => safe_div( bwd_bulk.total_bytes as f64, bwd_bulk.total_duration_us as f64 / 1_000_000.0, @@ -358,4 +365,4 @@ fn compute_flow_iats(fwd_packets: &[PacketData], bwd_packets: &[PacketData]) -> .windows(2) .map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64) .collect() -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/flow_tracker.rs b/mantis/src/detection/ml/flow_tracker.rs index 543c61d..bf76fad 100644 --- a/mantis/src/detection/ml/flow_tracker.rs +++ b/mantis/src/detection/ml/flow_tracker.rs @@ -103,13 +103,27 @@ impl FlowData { self.bwd_fin_seen = true; } } - if packet.tcp_flags().syn { self.syn_count += 1; } - if packet.tcp_flags().rst { self.rst_count += 1; } - if packet.tcp_flags().psh { self.psh_count += 1; } - if packet.tcp_flags().ack { self.ack_count += 1; } - if packet.tcp_flags().urg { self.urg_count += 1; } - if packet.tcp_flags().cwr { self.cwe_count += 1; } - if packet.tcp_flags().ece { self.ece_count += 1; } + if packet.tcp_flags().syn { + self.syn_count += 1; + } + if packet.tcp_flags().rst { + self.rst_count += 1; + } + if packet.tcp_flags().psh { + self.psh_count += 1; + } + if packet.tcp_flags().ack { + self.ack_count += 1; + } + if packet.tcp_flags().urg { + self.urg_count += 1; + } + if packet.tcp_flags().cwr { + self.cwe_count += 1; + } + if packet.tcp_flags().ece { + self.ece_count += 1; + } let iat = packet.timestamp_us().saturating_sub(self.last_packet_time); const IDLE_THRESHOLD_US: u64 = 1_000_000; @@ -166,9 +180,7 @@ impl FlowData { if packet.payload_length > 0 { // Idle break: discard helper, start fresh - if bulk_state.in_bulk - && packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US - { + if bulk_state.in_bulk && packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US { bulk_state.in_bulk = false; bulk_state.last_bulk_bytes = 0; bulk_state.last_bulk_packets = 0; @@ -193,14 +205,12 @@ impl FlowData { bulk_state.bulk_count += 1; bulk_state.total_packets += 4; bulk_state.total_bytes += bulk_state.last_bulk_bytes; - bulk_state.total_duration_us += packet.timestamp_us - .saturating_sub(bulk_state.last_bulk_start_us); + bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(bulk_state.last_bulk_start_us); } else if bulk_state.last_bulk_packets > 4 { // Each subsequent packet adds incrementally bulk_state.total_packets += 1; bulk_state.total_bytes += packet.length as u64; - bulk_state.total_duration_us += packet.timestamp_us - .saturating_sub(prev_us); + bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(prev_us); } } } else { @@ -279,7 +289,8 @@ impl FlowTracker { let initiator_direction = if is_forward { direction } else { direction.flip() }; - let flow = self.flows + let flow = self + .flows .entry(actual_key.clone()) .or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction)); @@ -305,10 +316,8 @@ impl FlowTracker { .duration_since(time::UNIX_EPOCH) .map(|d| d.as_micros() as u64) .unwrap_or(0); - self.flows.retain(|_, flow| { - !flow.is_finished() - && now.saturating_sub(flow.last_time_us) < max_age_us - }); + self.flows + .retain(|_, flow| !flow.is_finished() && now.saturating_sub(flow.last_time_us) < max_age_us); } pub fn flow_count(&self) -> usize { @@ -357,4 +366,4 @@ fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) } None -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/inference.rs b/mantis/src/detection/ml/inference.rs index 227e731..04520fd 100644 --- a/mantis/src/detection/ml/inference.rs +++ b/mantis/src/detection/ml/inference.rs @@ -1,5 +1,5 @@ -use std::collections::VecDeque; use std::collections::HashMap; +use std::collections::VecDeque; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -90,10 +90,7 @@ impl Inference { } let t2 = Instant::now(); - let ae_input = Array3::from_shape_fn( - (1, window_size, feat_len), - |(_, t, f)| sequence[t][f], - ); + let ae_input = Array3::from_shape_fn((1, window_size, feat_len), |(_, t, f)| sequence[t][f]); let t3 = Instant::now(); let ae_score = match self.run_autoencoder(&ae_input) { @@ -114,14 +111,19 @@ impl Inference { )); let pad = window_size - buf_len_snapshot; - let rows = sequence.iter().enumerate().map(|(t, row)| { - if t < pad { - format!(" t{t:02}: [pad]") - } else { - let vals = row.iter().map(|v| format!("{v:7.3}")).collect::>().join(" "); - format!(" t{t:02}: [{vals}]") - } - }).collect::>().join("\n"); + let rows = sequence + .iter() + .enumerate() + .map(|(t, row)| { + if t < pad { + format!(" t{t:02}: [pad]") + } else { + let vals = row.iter().map(|v| format!("{v:7.3}")).collect::>().join(" "); + format!(" t{t:02}: [{vals}]") + } + }) + .collect::>() + .join("\n"); log!(MLLog::WindowDebug( flow.flow_key.src_ip.clone(), pad, @@ -180,4 +182,4 @@ impl Inference { Ok(mse) } -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/mod.rs b/mantis/src/detection/ml/mod.rs index b3ddcd4..dd7d715 100644 --- a/mantis/src/detection/ml/mod.rs +++ b/mantis/src/detection/ml/mod.rs @@ -1,8 +1,8 @@ -pub mod model_loader; -pub mod config_loader; -pub mod flow_tracker; -pub mod feature_extractor; -pub mod inference; -pub mod engine; pub mod aggregator; +pub mod config_loader; +pub mod engine; +pub mod feature_extractor; +pub mod flow_tracker; +pub mod inference; +pub mod model_loader; pub mod traffic_logger; diff --git a/mantis/src/detection/ml/model_loader.rs b/mantis/src/detection/ml/model_loader.rs index 87d57ea..99f2e31 100644 --- a/mantis/src/detection/ml/model_loader.rs +++ b/mantis/src/detection/ml/model_loader.rs @@ -18,7 +18,8 @@ impl MLModels { { log!(MLLog::BackendNativeOrt); ort::init_from(PathBuf::from(env!("ONNXRUNTIME_PATH"))) - .map_err(|_| MLError::InitializeFailed)?.commit(); + .map_err(|_| MLError::InitializeFailed)? + .commit(); } #[cfg(feature = "tract-backend")] { @@ -35,9 +36,13 @@ impl MLModels { let model_path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(model_name); Session::builder() - .map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })? + .map_err(|_| MLError::ModelLoadFailed { + path: model_path.clone(), + })? .with_optimization_level(GraphOptimizationLevel::All) - .map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })? + .map_err(|_| MLError::ModelLoadFailed { + path: model_path.clone(), + })? .commit_from_file(&model_path) .map_err(|_| MLError::ModelLoadFailed { path: model_path }) } @@ -55,4 +60,4 @@ impl MLModels { _ => "unknown model".to_string(), } } -} \ No newline at end of file +} diff --git a/mantis/src/detection/ml/traffic_logger.rs b/mantis/src/detection/ml/traffic_logger.rs index e2e4954..4013cbd 100644 --- a/mantis/src/detection/ml/traffic_logger.rs +++ b/mantis/src/detection/ml/traffic_logger.rs @@ -1,11 +1,11 @@ use std::fs::{self, OpenOptions}; use std::io::{BufWriter, Write}; -use std::path::{PathBuf, Path}; +use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; use chrono::Local; -use crossbeam::channel::{bounded, Sender, TrySendError, RecvTimeoutError}; +use crossbeam::channel::{RecvTimeoutError, Sender, TrySendError, bounded}; use macros::log; use crate::model::error::ml::MLError; diff --git a/mantis/src/detection/mod.rs b/mantis/src/detection/mod.rs index 12c65bf..ff61bce 100644 --- a/mantis/src/detection/mod.rs +++ b/mantis/src/detection/mod.rs @@ -1,3 +1,3 @@ pub mod fusion; pub mod ml; -pub mod suricata; \ No newline at end of file +pub mod suricata; diff --git a/mantis/src/detection/suricata/engine.rs b/mantis/src/detection/suricata/engine.rs index e704022..3d63bae 100644 --- a/mantis/src/detection/suricata/engine.rs +++ b/mantis/src/detection/suricata/engine.rs @@ -6,16 +6,15 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -use crossbeam::channel::{bounded, Sender}; +use crossbeam::channel::{Sender, bounded}; use macros::log; +use super::output; use crate::detection::fusion::FusionEngine; use crate::model::config::SuricataConfig; use crate::model::error::suricata::SuricataError; use crate::model::log::suricata::SuricataLog; -use super::output; - const MIRROR_IFACE: &str = "mantis-mirror"; const MIRROR_PEER: &str = "mantis-peer"; const SURICATA_LOG: &str = "/tmp/suricata.log"; @@ -42,10 +41,12 @@ impl SuricataEngine { output::start_eve_reader(path, fusion); } - let rule_path_str = rule_path.to_str() - .ok_or_else(|| SuricataError::InvalidPath { path: rule_path.display().to_string() })?; - let eve_socket_str = eve_socket.to_str() - .ok_or_else(|| SuricataError::InvalidPath { path: eve_socket.display().to_string() })?; + let rule_path_str = rule_path.to_str().ok_or_else(|| SuricataError::InvalidPath { + path: rule_path.display().to_string(), + })?; + let eve_socket_str = eve_socket.to_str().ok_or_else(|| SuricataError::InvalidPath { + path: eve_socket.display().to_string(), + })?; let suppress = Self::generate_suppress(&config.suppress); let suppress_fd = Self::yaml_to_memfd(&suppress)?; @@ -73,7 +74,9 @@ impl SuricataEngine { .spawn(|| { let log_path = Path::new(SURICATA_LOG); for _ in 0..100 { - if log_path.exists() { break; } + if log_path.exists() { + break; + } thread::sleep(Duration::from_millis(100)); } let file = match std::fs::File::open(log_path) { @@ -96,17 +99,20 @@ impl SuricataEngine { if inotify_fd >= 0 { // Block until Suricata writes more data. let mut buf = [0u8; 64]; - let n = unsafe { - libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) - }; - if n <= 0 { break; } + let n = + unsafe { libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; + if n <= 0 { + break; + } } else { thread::sleep(Duration::from_millis(50)); } } Ok(_) => { let trimmed = line.trim_end().to_string(); - if trimmed.is_empty() { continue; } + if trimmed.is_empty() { + continue; + } let lower = trimmed.to_ascii_lowercase(); if lower.starts_with("error") || lower.starts_with("critical") { log!(SuricataLog::ProcessError { line: trimmed }); @@ -144,7 +150,9 @@ impl SuricataEngine { sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be(); sll.sll_ifindex = ifindex as i32; - log!(SuricataLog::MirrorReady { iface: MIRROR_IFACE.into() }); + log!(SuricataLog::MirrorReady { + iface: MIRROR_IFACE.into() + }); while let Ok(data) = rx.recv() { unsafe { @@ -166,7 +174,10 @@ impl SuricataEngine { log!(SuricataLog::Initialized); - Ok(Arc::new(Self { tx, child: std::sync::Mutex::new(child) })) + Ok(Arc::new(Self { + tx, + child: std::sync::Mutex::new(child), + })) } /* Non-blocking: drops silently when the channel is full under load. */ @@ -187,25 +198,31 @@ impl SuricataEngine { fn generate_yaml(config: &SuricataConfig, rule_path: &str, eve_socket: &str, suppress_path: &str) -> String { let threading = match (config.worker_cpu_set, config.management_cpu) { (None, None) => r#"threading: - set-cpu-affinity: no"#.to_string(), + set-cpu-affinity: no"# + .to_string(), (worker, mgmt) => { let mgmt_cpu = mgmt.unwrap_or(0); let worker_block = match worker { - Some([start, end]) => format!(r#" - worker-cpu-set: + Some([start, end]) => format!( + r#" - worker-cpu-set: cpu: [ "{start}-{end}" ] - mode: "balanced""#), + mode: "balanced""# + ), None => String::new(), }; - format!(r#"threading: + format!( + r#"threading: set-cpu-affinity: yes cpu-affinity: - management-cpu-set: cpu: [ {mgmt_cpu} ] -{worker_block}"#) +{worker_block}"# + ) } }; - format!(r#"%YAML 1.1 + format!( + r#"%YAML 1.1 --- vars: address-groups: @@ -298,23 +315,21 @@ legacy: host-mode: sniffer-only "#, - home_net = config.home_net, - rule_path = rule_path, - eve_socket = eve_socket, - suppress_path = suppress_path, - log_path = SURICATA_LOG, - iface = MIRROR_PEER, - threads = config.af_packet_threads, - ring_size = config.af_packet_ring_size, - block_size = config.af_packet_block_size, - threading = threading, + home_net = config.home_net, + rule_path = rule_path, + eve_socket = eve_socket, + suppress_path = suppress_path, + log_path = SURICATA_LOG, + iface = MIRROR_PEER, + threads = config.af_packet_threads, + ring_size = config.af_packet_ring_size, + block_size = config.af_packet_block_size, + threading = threading, ) } fn yaml_to_memfd(yaml: &str) -> Result { - let fd = unsafe { - libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0) - }; + let fd = unsafe { libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0) }; if fd < 0 { let errno = unsafe { *libc::__errno_location() }; return Err(SuricataError::MirrorSetupFailed { @@ -322,9 +337,7 @@ host-mode: sniffer-only }); } let bytes = yaml.as_bytes(); - let written = unsafe { - libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) - }; + let written = unsafe { libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) }; if written < 0 { unsafe { libc::close(fd) }; let errno = unsafe { *libc::__errno_location() }; @@ -351,7 +364,10 @@ host-mode: sniffer-only .map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?; } - log!(SuricataLog::VethCreated { iface: MIRROR_IFACE.into(), peer: MIRROR_PEER.into() }); + log!(SuricataLog::VethCreated { + iface: MIRROR_IFACE.into(), + peer: MIRROR_PEER.into() + }); Ok(()) } @@ -366,13 +382,7 @@ host-mode: sniffer-only } fn open_raw_socket() -> Result { - let fd = unsafe { - libc::socket( - libc::AF_PACKET, - libc::SOCK_RAW, - (libc::ETH_P_ALL as u16).to_be() as i32, - ) - }; + let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, (libc::ETH_P_ALL as u16).to_be() as i32) }; if fd < 0 { let errno = unsafe { *libc::__errno_location() }; return Err(SuricataError::MirrorSetupFailed { @@ -390,4 +400,4 @@ impl Drop for SuricataEngine { } let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output(); } -} \ No newline at end of file +} diff --git a/mantis/src/detection/suricata/mod.rs b/mantis/src/detection/suricata/mod.rs index e1095f0..82fbe20 100644 --- a/mantis/src/detection/suricata/mod.rs +++ b/mantis/src/detection/suricata/mod.rs @@ -1,3 +1,3 @@ -mod output; pub mod engine; +mod output; pub use engine::SuricataEngine; diff --git a/mantis/src/detection/suricata/output.rs b/mantis/src/detection/suricata/output.rs index a3eb419..6024488 100644 --- a/mantis/src/detection/suricata/output.rs +++ b/mantis/src/detection/suricata/output.rs @@ -78,9 +78,7 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc Result<(), Error> { let mut system = System::new().await?; system.run().await?; system.terminate().await?; - + drop(system); std::process::exit(0); -} \ No newline at end of file +} diff --git a/mantis/src/model/config.rs b/mantis/src/model/config.rs index fc489bf..7f4015a 100644 --- a/mantis/src/model/config.rs +++ b/mantis/src/model/config.rs @@ -21,9 +21,15 @@ pub struct SuricataConfig { pub suppress: Vec, } -fn default_af_threads() -> String { "auto".to_string() } -fn default_af_ring_size() -> u32 { 2048 } -fn default_af_block_size() -> u32 { 131072 } +fn default_af_threads() -> String { + "auto".to_string() +} +fn default_af_ring_size() -> u32 { + 2048 +} +fn default_af_block_size() -> u32 { + 131072 +} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Config { @@ -61,6 +67,25 @@ pub struct Config { /// Suricata rule engine config. If absent, the rule engine is disabled. pub suricata: Option, + /// Auth system config. If absent, auth is disabled and all routes are public. + pub auth: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AuthConfig { + pub jwt_secret: String, + pub db_key: String, + #[serde(default = "default_token_ttl")] + pub token_ttl_secs: u64, + #[serde(default = "default_admin_password")] + pub default_admin_password: String, +} + +fn default_token_ttl() -> u64 { + 86400 +} +fn default_admin_password() -> String { + "admin".to_string() } fn default_fusion_mode() -> String { @@ -73,4 +98,4 @@ fn default_fusion_window_secs() -> u64 { fn default_ae_threshold_method() -> String { "95".to_string() -} \ No newline at end of file +} diff --git a/mantis/src/model/error/auth.rs b/mantis/src/model/error/auth.rs new file mode 100644 index 0000000..8910b56 --- /dev/null +++ b/mantis/src/model/error/auth.rs @@ -0,0 +1,21 @@ +use macros::traceable; + +traceable! { + AuthError { + #[no_source] + #[error("Database error: {msg}")] + DBError { msg: String } => tracing::Level::ERROR, + + #[no_source] + #[error("Invalid credentials")] + InvalidCredentials => tracing::Level::WARN, + + #[no_source] + #[error("Token expired or invalid")] + InvalidToken => tracing::Level::WARN, + + #[no_source] + #[error("Password hashing failed")] + HashError => tracing::Level::ERROR, + } +} diff --git a/mantis/src/model/error/ebpf.rs b/mantis/src/model/error/ebpf.rs index c59a1f7..dfb2387 100644 --- a/mantis/src/model/error/ebpf.rs +++ b/mantis/src/model/error/ebpf.rs @@ -70,4 +70,3 @@ traceable! { FlowMapKeyMissing { direction: String, flow_direction: String, time_type: String } => tracing::Level::ERROR, } } - diff --git a/mantis/src/model/error/http.rs b/mantis/src/model/error/http.rs index af167e5..28f7bc5 100644 --- a/mantis/src/model/error/http.rs +++ b/mantis/src/model/error/http.rs @@ -8,7 +8,8 @@ traceable! { #[error("Http Server panic")] ServerPanic => tracing::Level::ERROR, - #[error("WebSocket error")] - WebSocketError => tracing::Level::ERROR, + #[no_source] + #[error("WebSocket error: {msg}")] + WebSocketError { msg: String } => tracing::Level::ERROR, } } diff --git a/mantis/src/model/error/misc.rs b/mantis/src/model/error/misc.rs index cf7db5a..886f716 100644 --- a/mantis/src/model/error/misc.rs +++ b/mantis/src/model/error/misc.rs @@ -26,4 +26,4 @@ traceable! { #[error("Failed to create traffic log file '{path}': {reason}")] TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR, } -} \ No newline at end of file +} diff --git a/mantis/src/model/error/ml.rs b/mantis/src/model/error/ml.rs index a22bb4b..752d73a 100644 --- a/mantis/src/model/error/ml.rs +++ b/mantis/src/model/error/ml.rs @@ -48,4 +48,4 @@ traceable! { #[error("Failed to write CSV row: {reason}")] TrafficLogWriteFailed { reason: String } => tracing::Level::WARN, } -} \ No newline at end of file +} diff --git a/mantis/src/model/error/mod.rs b/mantis/src/model/error/mod.rs index 2100338..be1b678 100644 --- a/mantis/src/model/error/mod.rs +++ b/mantis/src/model/error/mod.rs @@ -1,13 +1,15 @@ +pub mod auth; pub mod ebpf; pub mod http; pub mod io; pub mod misc; pub mod ml; -pub mod system; pub mod suricata; +pub mod system; use serde::{Deserialize, Serialize}; +use crate::model::error::auth::AuthError; use crate::model::error::ebpf::EbpfError; use crate::model::error::http::HttpError; use crate::model::error::io::IOError; @@ -18,6 +20,8 @@ use crate::model::error::system::SystemError; #[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)] pub enum Error { + #[error("{0}")] + Auth(AuthError), #[error("{0}")] Ebpf(EbpfError), #[error("{0}")] @@ -34,6 +38,12 @@ pub enum Error { System(SystemError), } +impl From for Error { + fn from(error: AuthError) -> Self { + Self::Auth(error) + } +} + impl From for Error { fn from(error: EbpfError) -> Self { Self::Ebpf(error) @@ -74,4 +84,4 @@ impl From for Error { fn from(error: SuricataError) -> Self { Self::Suricata(error) } -} \ No newline at end of file +} diff --git a/mantis/src/model/geo_stats.rs b/mantis/src/model/geo_stats.rs index 4bf850e..b92c864 100644 --- a/mantis/src/model/geo_stats.rs +++ b/mantis/src/model/geo_stats.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use common::model::flow_stats::FlowStats; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize)] pub struct FlowStatsWithGeo { diff --git a/mantis/src/model/ip_address.rs b/mantis/src/model/ip_address.rs index 488a082..06b4999 100644 --- a/mantis/src/model/ip_address.rs +++ b/mantis/src/model/ip_address.rs @@ -37,17 +37,11 @@ impl NativeConvert for AddrPortV4 { type Native = SocketAddrV4; fn into_native(self) -> Self::Native { - SocketAddrV4::new( - Ipv4Addr::from(u32::from_be(self.ip())), - self.port() - ) + SocketAddrV4::new(Ipv4Addr::from(u32::from_be(self.ip())), self.port()) } fn from_native(native: Self::Native) -> Self { - AddrPortV4::new( - native.ip().to_bits().to_be(), - native.port() - ) + AddrPortV4::new(native.ip().to_bits().to_be(), native.port()) } } @@ -55,18 +49,10 @@ impl NativeConvert for AddrPortV6 { type Native = SocketAddrV6; fn into_native(self) -> Self::Native { - SocketAddrV6::new( - Ipv6Addr::from(u128::from_be(self.ip())), - self.port(), - 0, - 0 - ) + SocketAddrV6::new(Ipv6Addr::from(u128::from_be(self.ip())), self.port(), 0, 0) } fn from_native(native: Self::Native) -> Self { - AddrPortV6::new( - native.ip().to_bits().to_be(), - native.port() - ) + AddrPortV6::new(native.ip().to_bits().to_be(), native.port()) } -} \ No newline at end of file +} diff --git a/mantis/src/model/log/auth.rs b/mantis/src/model/log/auth.rs new file mode 100644 index 0000000..bffa47a --- /dev/null +++ b/mantis/src/model/log/auth.rs @@ -0,0 +1,18 @@ +use macros::loggable; +use tracing; + +loggable! { + AuthLog { + #[error("Auth DB initialized at {path}")] + DbInitialized { path: String } => tracing::Level::INFO, + + #[error("Default admin account created")] + DefaultAdminCreated => tracing::Level::INFO, + + #[error("Login successful for user: {username}")] + LoginSuccess { username: String } => tracing::Level::INFO, + + #[error("Login failed for user: {username}")] + LoginFailed { username: String } => tracing::Level::WARN, + } +} diff --git a/mantis/src/model/log/ebpf.rs b/mantis/src/model/log/ebpf.rs index 3f6c6dc..3cd72aa 100644 --- a/mantis/src/model/log/ebpf.rs +++ b/mantis/src/model/log/ebpf.rs @@ -26,7 +26,7 @@ loggable! { #[error("No frames available for TX")] NoFramesAvailable => tracing::Level::WARN, - + #[error("TX wakeup failed: {error}")] TXWakeupFailed { error: String } => tracing::Level::WARN, @@ -51,4 +51,4 @@ loggable! { #[error("Fill queue incomplete: produced {produced}, expected {expected}")] FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN, } -} \ No newline at end of file +} diff --git a/mantis/src/model/log/ml.rs b/mantis/src/model/log/ml.rs index a113b52..39982d6 100644 --- a/mantis/src/model/log/ml.rs +++ b/mantis/src/model/log/ml.rs @@ -79,4 +79,4 @@ loggable! { WindowDebug { src: String, pad: usize, window_size: usize, ae_score: f32, rows: String } => tracing::Level::DEBUG, } -} \ No newline at end of file +} diff --git a/mantis/src/model/log/mod.rs b/mantis/src/model/log/mod.rs index 4ae0a9e..32a26ac 100644 --- a/mantis/src/model/log/mod.rs +++ b/mantis/src/model/log/mod.rs @@ -1,7 +1,8 @@ +pub mod auth; pub mod ebpf; -pub mod http; -pub mod ml; -pub mod system; -pub mod misc; pub mod health; +pub mod http; +pub mod misc; +pub mod ml; pub mod suricata; +pub mod system; diff --git a/mantis/src/model/log/suricata.rs b/mantis/src/model/log/suricata.rs index ebbd808..35e1355 100644 --- a/mantis/src/model/log/suricata.rs +++ b/mantis/src/model/log/suricata.rs @@ -33,4 +33,4 @@ loggable! { #[error("[suricata] {line}")] ProcessError { line: String } => tracing::Level::ERROR, } -} \ No newline at end of file +} diff --git a/mantis/src/model/log/system.rs b/mantis/src/model/log/system.rs index 22588c7..c4bce53 100644 --- a/mantis/src/model/log/system.rs +++ b/mantis/src/model/log/system.rs @@ -28,4 +28,4 @@ loggable! { TrafficLoggingEnabled { path: String } => tracing::Level::INFO, } -} \ No newline at end of file +} diff --git a/mantis/src/model/ml_detection.rs b/mantis/src/model/ml_detection.rs index 74d224f..c383ffb 100644 --- a/mantis/src/model/ml_detection.rs +++ b/mantis/src/model/ml_detection.rs @@ -236,4 +236,4 @@ impl UnifiedAlert { rule_msg: Some(m.msg.clone()), } } -} \ No newline at end of file +} diff --git a/mantis/src/model/mod.rs b/mantis/src/model/mod.rs index 277a82e..566f6f0 100644 --- a/mantis/src/model/mod.rs +++ b/mantis/src/model/mod.rs @@ -2,10 +2,10 @@ pub mod config; pub mod direction; pub mod error; pub mod geo_stats; +pub mod health; pub mod ip_address; pub mod list_type; pub mod log; -pub mod time_type; pub mod ml_detection; -pub mod health; pub mod rule_detection; +pub mod time_type; diff --git a/mantis/src/utils/ip_address.rs b/mantis/src/utils/ip_address.rs index 2203b23..e8e326b 100644 --- a/mantis/src/utils/ip_address.rs +++ b/mantis/src/utils/ip_address.rs @@ -13,12 +13,7 @@ pub fn convert_ports_to_vec(ports: [u16; MAX_RULES_PORT]) -> Vec { pub fn is_private_ip(ip: &IpAddr) -> bool { match ip { - IpAddr::V4(v4) => { - v4.is_private() - || v4.is_loopback() - || v4.is_link_local() - || v4.is_broadcast() - } + IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local() || v4.is_broadcast(), IpAddr::V6(v6) => { v6.is_loopback() || v6.is_unique_local() // fc00::/7 @@ -26,4 +21,4 @@ pub fn is_private_ip(ip: &IpAddr) -> bool { || v6.is_multicast() } } -} \ No newline at end of file +} diff --git a/mantis/src/utils/logging.rs b/mantis/src/utils/logging.rs index b88090c..18b0904 100644 --- a/mantis/src/utils/logging.rs +++ b/mantis/src/utils/logging.rs @@ -1,20 +1,20 @@ use std::fs; + use tracing::Level; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::filter::EnvFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use crate::model::error::io::IOError; use crate::model::error::Error; +use crate::model::error::io::IOError; pub struct Logging; impl Logging { pub fn initialize() -> Result<(), Error> { let log_directory = "logs"; - fs::create_dir_all(log_directory) - .map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?; + fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?; let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "Mantis"); diff --git a/mantis/src/utils/mod.rs b/mantis/src/utils/mod.rs index 50d5f76..b3ee894 100644 --- a/mantis/src/utils/mod.rs +++ b/mantis/src/utils/mod.rs @@ -1,7 +1,7 @@ -pub mod logging; -pub mod static_files; pub mod boot_time; -pub mod packet_parser; pub mod cpu_affinity; +pub mod logging; +pub mod packet_parser; +pub mod static_files; -pub mod ip_address; \ No newline at end of file +pub mod ip_address; diff --git a/mantis/src/utils/packet_parser.rs b/mantis/src/utils/packet_parser.rs index a24fdb0..be4f2a9 100644 --- a/mantis/src/utils/packet_parser.rs +++ b/mantis/src/utils/packet_parser.rs @@ -184,4 +184,4 @@ pub fn format_ipv6(addr: u128) -> String { bytes[14], bytes[15] ) -} \ No newline at end of file +} diff --git a/mantis/src/web/api/auth.rs b/mantis/src/web/api/auth.rs new file mode 100644 index 0000000..328d055 --- /dev/null +++ b/mantis/src/web/api/auth.rs @@ -0,0 +1,105 @@ +use axum::Json; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::{ + Router, + routing::{get, post}, +}; +use chrono::Utc; +use jsonwebtoken::{EncodingKey, Header, encode}; +use macros::log; +use serde::{Deserialize, Serialize}; + +use crate::core::app_state::AppState; +use crate::core::infrastructure::app_db::verify_password; +use crate::model::log::auth::AuthLog; +use crate::web::middleware::auth::{AuthenticatedUser, Claims}; + +pub fn router() -> Router { + Router::new() + .route("/login", post(login)) + .route("/me", get(me)) + .route("/logout", post(logout)) +} + +#[derive(Deserialize)] +struct LoginRequest { + username: String, + password: String, +} + +#[derive(Serialize)] +struct LoginResponse { + token: String, +} + +#[derive(Serialize)] +struct MeResponse { + id: String, + username: String, + role: String, +} + +async fn login(State(state): State, Json(body): Json) -> impl IntoResponse { + let auth_cfg = match state.app_config.auth.as_ref() { + Some(c) => c, + None => return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response(), + }; + let db = match &state.app_db { + Some(db) => db, + None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(), + }; + + let account = match db.find_account_by_username(&body.username) { + Ok(Some(a)) => a, + Ok(None) => { + log!(AuthLog::LoginFailed { + username: body.username.clone() + }); + return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(); + } + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(), + }; + + if !verify_password(&body.password, &account.password_hash) { + log!(AuthLog::LoginFailed { + username: body.username.clone() + }); + return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(); + } + + let exp = (Utc::now().timestamp() as usize) + (auth_cfg.token_ttl_secs as usize); + let claims = Claims { + sub: account.id.clone(), + username: account.username.clone(), + role: account.role.clone(), + exp, + }; + + match encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()), + ) { + Ok(token) => { + log!(AuthLog::LoginSuccess { + username: account.username + }); + Json(LoginResponse { token }).into_response() + } + Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Token generation failed").into_response(), + } +} + +async fn me(user: AuthenticatedUser) -> impl IntoResponse { + Json(MeResponse { + id: user.0.sub, + username: user.0.username, + role: user.0.role, + }) +} + +async fn logout(_user: AuthenticatedUser) -> impl IntoResponse { + StatusCode::OK +} diff --git a/mantis/src/web/api/control/access_control.rs b/mantis/src/web/api/control/access_control.rs index 37bd1a4..600bb33 100644 --- a/mantis/src/web/api/control/access_control.rs +++ b/mantis/src/web/api/control/access_control.rs @@ -1,93 +1,89 @@ use std::net::{SocketAddrV4, SocketAddrV6}; -use actix_web::{delete, get, put, web, HttpResponse, Responder, Scope}; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::{delete, get, put}; +use axum::{Json, Router}; -use crate::core::ebpf::access_control::AccessControl; +use crate::core::app_state::AppState; use crate::model::direction::FlowDirection; use crate::model::list_type::ListType; -pub fn initialize() -> Scope { - web::scope("/access_control") - .service(get_ipv4_list) - .service(get_ipv6_list) - .service(add_ipv4_list) - .service(add_ipv6_list) - .service(remove_ipv4_list) - .service(remove_ipv6_list) +pub fn router() -> Router { + Router::new() + .route( + "/ipv4/{direction}/{list_type}", + get(get_ipv4_list).put(add_ipv4_list).delete(remove_ipv4_list), + ) + .route( + "/ipv6/{direction}/{list_type}", + get(get_ipv6_list).put(add_ipv6_list).delete(remove_ipv6_list), + ) } -#[get("/ipv4/{direction}/{list_type}")] async fn get_ipv4_list( - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let (direction, list_type) = path.into_inner(); - let list = access_control.get_ipv4_list(direction, list_type).await; - HttpResponse::Ok().json(list) + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, +) -> impl IntoResponse { + Json(state.access_control.get_ipv4_list(direction, list_type).await) } -#[get("/ipv6/{direction}/{list_type}")] async fn get_ipv6_list( - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let (direction, list_type) = path.into_inner(); - let list = access_control.get_ipv6_list(direction, list_type).await; - HttpResponse::Ok().json(list) + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, +) -> impl IntoResponse { + Json(state.access_control.get_ipv6_list(direction, list_type).await) } -#[put("/ipv4/{direction}/{list_type}")] async fn add_ipv4_list( - address: web::Json, - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let address = address.into_inner(); - let (direction, list_type) = path.into_inner(); - match access_control.add_ipv4_list(direction, list_type, address).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, + Json(address): Json, +) -> impl IntoResponse { + match state.access_control.add_ipv4_list(direction, list_type, address).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[put("/ipv6/{direction}/{list_type}")] async fn add_ipv6_list( - address: web::Json, - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let address = address.into_inner(); - let (direction, list_type) = path.into_inner(); - match access_control.add_ipv6_list(direction, list_type, address).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, + Json(address): Json, +) -> impl IntoResponse { + match state.access_control.add_ipv6_list(direction, list_type, address).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv4/{direction}/{list_type}")] async fn remove_ipv4_list( - address: web::Json, - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let address = address.into_inner(); - let (direction, list_type) = path.into_inner(); - match access_control.remove_ipv4_list(direction, list_type, address).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, + Json(address): Json, +) -> impl IntoResponse { + match state + .access_control + .remove_ipv4_list(direction, list_type, address) + .await + { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv6/{direction}/{list_type}")] async fn remove_ipv6_list( - address: web::Json, - path: web::Path<(FlowDirection, ListType)>, - access_control: web::Data, -) -> impl Responder { - let address = address.into_inner(); - let (direction, list_type) = path.into_inner(); - match access_control.remove_ipv6_list(direction, list_type, address).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + Path((direction, list_type)): Path<(FlowDirection, ListType)>, + State(state): State, + Json(address): Json, +) -> impl IntoResponse { + match state + .access_control + .remove_ipv6_list(direction, list_type, address) + .await + { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } diff --git a/mantis/src/web/api/control/mod.rs b/mantis/src/web/api/control/mod.rs index d3036e2..914eee3 100644 --- a/mantis/src/web/api/control/mod.rs +++ b/mantis/src/web/api/control/mod.rs @@ -2,11 +2,13 @@ pub mod access_control; pub mod service; pub mod statistics; -use actix_web::{web, Scope}; +use axum::Router; -pub fn initialize() -> Scope { - web::scope("/ebpf") - .service(access_control::initialize()) - .service(service::initialize()) - .service(statistics::initialize()) +use crate::core::app_state::AppState; + +pub fn router() -> Router { + Router::new() + .nest("/access_control", access_control::router()) + .nest("/service", service::router()) + .nest("/statistics", statistics::router()) } diff --git a/mantis/src/web/api/control/service.rs b/mantis/src/web/api/control/service.rs index 3b1df29..fb5a73d 100644 --- a/mantis/src/web/api/control/service.rs +++ b/mantis/src/web/api/control/service.rs @@ -1,251 +1,243 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; -use actix_web::{delete, get, post, put, web, HttpResponse, Responder, Scope}; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::{delete, get, post, put}; +use axum::{Json, Router}; use common::model::http_method::HttpMethod; -use crate::core::ebpf::service::Service; +use crate::core::app_state::AppState; -pub fn initialize() -> Scope { - web::scope("/service") - .service(get_ipv4_http_service) - .service(get_ipv6_http_service) - .service(add_ipv4_http_service) - .service(add_ipv6_http_service) - .service(remove_ipv4_http_service) - .service(remove_ipv6_http_service) - .service(is_ssh_white_list_enable) - .service(enable_ssh_white_list) - .service(disable_ssh_white_list) - .service(get_ipv4_ssh_service) - .service(get_ipv6_ssh_service) - .service(add_ipv4_ssh_service) - .service(add_ipv6_ssh_service) - .service(remove_ipv4_ssh_service) - .service(remove_ipv6_ssh_service) - .service(get_ipv4_ssh_white_list) - .service(get_ipv6_ssh_white_list) - .service(add_ipv4_ssh_white_list) - .service(add_ipv6_ssh_white_list) - .service(remove_ipv4_ssh_white_list) - .service(remove_ipv6_ssh_white_list) - .service(get_ipv4_ssh_black_list) - .service(get_ipv6_ssh_black_list) - .service(add_ipv4_ssh_black_list) - .service(add_ipv6_ssh_black_list) - .service(remove_ipv4_ssh_black_list) - .service(remove_ipv6_ssh_black_list) +pub fn router() -> Router { + Router::new() + .route( + "/ipv4/http_service", + get(get_ipv4_http_service) + .put(add_ipv4_http_service) + .delete(remove_ipv4_http_service), + ) + .route( + "/ipv6/http_service", + get(get_ipv6_http_service) + .put(add_ipv6_http_service) + .delete(remove_ipv6_http_service), + ) + .route("/ssh_white_list", get(is_ssh_white_list_enable)) + .route("/ssh_white_list/enable", post(enable_ssh_white_list)) + .route("/ssh_white_list/disable", post(disable_ssh_white_list)) + .route( + "/ipv4/ssh_service", + get(get_ipv4_ssh_service) + .put(add_ipv4_ssh_service) + .delete(remove_ipv4_ssh_service), + ) + .route( + "/ipv6/ssh_service", + get(get_ipv6_ssh_service) + .put(add_ipv6_ssh_service) + .delete(remove_ipv6_ssh_service), + ) + .route( + "/ipv4/ssh_white_list", + get(get_ipv4_ssh_white_list) + .put(add_ipv4_ssh_white_list) + .delete(remove_ipv4_ssh_white_list), + ) + .route( + "/ipv6/ssh_white_list", + get(get_ipv6_ssh_white_list) + .put(add_ipv6_ssh_white_list) + .delete(remove_ipv6_ssh_white_list), + ) + .route( + "/ipv4/ssh_black_list", + get(get_ipv4_ssh_black_list) + .put(add_ipv4_ssh_black_list) + .delete(remove_ipv4_ssh_black_list), + ) + .route( + "/ipv6/ssh_black_list", + get(get_ipv6_ssh_black_list) + .put(add_ipv6_ssh_black_list) + .delete(remove_ipv6_ssh_black_list), + ) } -#[get("/ipv4/http_service")] -async fn get_ipv4_http_service(service: web::Data) -> impl Responder { - let list = service.get_ipv4_http_service().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv4_http_service(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv4_http_service().await) } -#[get("/ipv6/http_service")] -async fn get_ipv6_http_service(service: web::Data) -> impl Responder { - let list = service.get_ipv6_http_service().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv6_http_service(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv6_http_service().await) } -#[put("/ipv4/http_service")] async fn add_ipv4_http_service( - payload: web::Json<(SocketAddrV4, Vec)>, - service: web::Data, -) -> impl Responder { - let (addr, methods) = payload.into_inner(); - match service.add_ipv4_http_service(addr, methods).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + State(state): State, + Json(payload): Json<(SocketAddrV4, Vec)>, +) -> impl IntoResponse { + let (addr, methods) = payload; + match state.service.add_ipv4_http_service(addr, methods).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[put("/ipv6/http_service")] async fn add_ipv6_http_service( - payload: web::Json<(SocketAddrV6, Vec)>, - service: web::Data, -) -> impl Responder { - let (addr, methods) = payload.into_inner(); - match service.add_ipv6_http_service(addr, methods).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + State(state): State, + Json(payload): Json<(SocketAddrV6, Vec)>, +) -> impl IntoResponse { + let (addr, methods) = payload; + match state.service.add_ipv6_http_service(addr, methods).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv4/http_service")] async fn remove_ipv4_http_service( - payload: web::Json<(SocketAddrV4, Vec)>, - service: web::Data, -) -> impl Responder { - let (addr, methods) = payload.into_inner(); - match service.remove_ipv4_http_service(addr, methods).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + State(state): State, + Json(payload): Json<(SocketAddrV4, Vec)>, +) -> impl IntoResponse { + let (addr, methods) = payload; + match state.service.remove_ipv4_http_service(addr, methods).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv6/http_service")] async fn remove_ipv6_http_service( - payload: web::Json<(SocketAddrV6, Vec)>, - service: web::Data, -) -> impl Responder { - let (addr, methods) = payload.into_inner(); - match service.remove_ipv6_http_service(addr, methods).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + State(state): State, + Json(payload): Json<(SocketAddrV6, Vec)>, +) -> impl IntoResponse { + let (addr, methods) = payload; + match state.service.remove_ipv6_http_service(addr, methods).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[get("/ssh_white_list")] -async fn is_ssh_white_list_enable(service: web::Data) -> impl Responder { - let enabled = service.is_ssh_white_list_enable().await; - HttpResponse::Ok().json(enabled) +async fn is_ssh_white_list_enable(State(state): State) -> impl IntoResponse { + Json(state.service.is_ssh_white_list_enable().await) } -#[post("/ssh_white_list/enable")] -async fn enable_ssh_white_list(service: web::Data) -> impl Responder { - match service.enable_ssh_white_list().await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn enable_ssh_white_list(State(state): State) -> impl IntoResponse { + match state.service.enable_ssh_white_list().await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[post("/ssh_white_list/disable")] -async fn disable_ssh_white_list(service: web::Data) -> impl Responder { - match service.disable_ssh_white_list().await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn disable_ssh_white_list(State(state): State) -> impl IntoResponse { + match state.service.disable_ssh_white_list().await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[get("/ipv4/ssh_service")] -async fn get_ipv4_ssh_service(service: web::Data) -> impl Responder { - let list = service.get_ipv4_ssh_service().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv4_ssh_service(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv4_ssh_service().await) } -#[get("/ipv6/ssh_service")] -async fn get_ipv6_ssh_service(service: web::Data) -> impl Responder { - let list = service.get_ipv6_ssh_service().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv6_ssh_service(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv6_ssh_service().await) } -#[put("/ipv4/ssh_service")] -async fn add_ipv4_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv4_ssh_service(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv4_ssh_service(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv4_ssh_service(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[put("/ipv6/ssh_service")] -async fn add_ipv6_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv6_ssh_service(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv6_ssh_service(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv6_ssh_service(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv4/ssh_service")] -async fn remove_ipv4_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv4_ssh_service(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv4_ssh_service(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv4_ssh_service(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv6/ssh_service")] -async fn remove_ipv6_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv6_ssh_service(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv6_ssh_service(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv6_ssh_service(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[get("/ipv4/ssh_white_list")] -async fn get_ipv4_ssh_white_list(service: web::Data) -> impl Responder { - let list = service.get_ipv4_ssh_white_list().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv4_ssh_white_list(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv4_ssh_white_list().await) } -#[get("/ipv6/ssh_white_list")] -async fn get_ipv6_ssh_white_list(service: web::Data) -> impl Responder { - let list = service.get_ipv6_ssh_white_list().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv6_ssh_white_list(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv6_ssh_white_list().await) } -#[put("/ipv4/ssh_white_list")] -async fn add_ipv4_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv4_ssh_white_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv4_ssh_white_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[put("/ipv6/ssh_white_list")] -async fn add_ipv6_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv6_ssh_white_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv6_ssh_white_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv4/ssh_white_list")] -async fn remove_ipv4_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv4_ssh_white_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv4_ssh_white_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv6/ssh_white_list")] -async fn remove_ipv6_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv6_ssh_white_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv6_ssh_white_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[get("/ipv4/ssh_black_list")] -async fn get_ipv4_ssh_black_list(service: web::Data) -> impl Responder { - let list = service.get_ipv4_ssh_black_list().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv4_ssh_black_list(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv4_ssh_black_list().await) } -#[get("/ipv6/ssh_black_list")] -async fn get_ipv6_ssh_black_list(service: web::Data) -> impl Responder { - let list = service.get_ipv6_ssh_black_list().await; - HttpResponse::Ok().json(web::Json(list)) +async fn get_ipv6_ssh_black_list(State(state): State) -> impl IntoResponse { + Json(state.service.get_ipv6_ssh_black_list().await) } -#[put("/ipv4/ssh_black_list")] -async fn add_ipv4_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv4_ssh_black_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv4_ssh_black_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[put("/ipv6/ssh_black_list")] -async fn add_ipv6_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn add_ipv6_ssh_black_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.add_ipv6_ssh_black_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv4/ssh_black_list")] -async fn remove_ipv4_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv4_ssh_black_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv4_ssh_black_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -#[delete("/ipv6/ssh_black_list")] -async fn remove_ipv6_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { - match service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await { - Ok(_) => HttpResponse::Ok().finish(), - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +async fn remove_ipv6_ssh_black_list(State(state): State, Json(addr): Json) -> impl IntoResponse { + match state.service.remove_ipv6_ssh_black_list(addr).await { + Ok(_) => StatusCode::OK.into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } diff --git a/mantis/src/web/api/control/statistics.rs b/mantis/src/web/api/control/statistics.rs index c0da53e..38198ec 100644 --- a/mantis/src/web/api/control/statistics.rs +++ b/mantis/src/web/api/control/statistics.rs @@ -1,76 +1,76 @@ -use std::sync::Arc; - -use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; +use axum::extract::ws::WebSocketUpgrade; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::get; +use axum::{Json, Router}; use macros::log; -use crate::core::ebpf::statistics::Statistics; -use crate::core::infrastructure::app_config::AppConfig; +use crate::core::app_state::AppState; use crate::model::direction::{Direction, FlowDirection}; use crate::model::time_type::TimeType; use crate::web::websocket::flow_websocket; -pub fn initialize() -> Scope { - web::scope("/statistics") - .service(get_ipv4_flow) - .service(get_ipv6_flow) - .service(websocket_ipv4) - .service(websocket_ipv6) +pub fn router() -> Router { + Router::new() + .route("/get/ipv4/{direction}/{flow_direction}/{time_type}", get(get_ipv4_flow)) + .route("/get/ipv6/{direction}/{flow_direction}/{time_type}", get(get_ipv6_flow)) + .route( + "/websocket/ipv4/{direction}/{flow_direction}/{time_type}", + get(websocket_ipv4), + ) + .route( + "/websocket/ipv6/{direction}/{flow_direction}/{time_type}", + get(websocket_ipv6), + ) } -#[get("/get/ipv4/{direction}/{flow_direction}/{time_type}")] async fn get_ipv4_flow( - path: web::Path<(Direction, FlowDirection, TimeType)>, - statistics: web::Data>, -) -> impl Responder { - let (direction, flow_direction, time_type) = path.into_inner(); - match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await { - Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)), + Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>, + State(state): State, +) -> impl IntoResponse { + match state + .statistics + .get_ipv4_flow_data(direction, flow_direction, time_type) + .await + { + Ok(data) => Json(data).into_response(), Err(e) => { log!(e); - HttpResponse::InternalServerError().finish() + StatusCode::INTERNAL_SERVER_ERROR.into_response() } } } -#[get("/get/ipv6/{direction}/{flow_direction}/{time_type}")] async fn get_ipv6_flow( - path: web::Path<(Direction, FlowDirection, TimeType)>, - statistics: web::Data>, -) -> impl Responder { - let (direction, flow_direction, time_type) = path.into_inner(); - match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await { - Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)), + Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>, + State(state): State, +) -> impl IntoResponse { + match state + .statistics + .get_ipv6_flow_data(direction, flow_direction, time_type) + .await + { + Ok(data) => Json(data).into_response(), Err(e) => { log!(e); - HttpResponse::InternalServerError().finish() + StatusCode::INTERNAL_SERVER_ERROR.into_response() } } } -#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")] async fn websocket_ipv4( - req: HttpRequest, - stream: web::Payload, - path: web::Path<(Direction, FlowDirection, TimeType)>, - app_config: web::Data, - statistics: web::Data, -) -> impl Responder { - match flow_websocket::websocket_ipv4_flow(req, stream, path, app_config, statistics).await { - Ok(response) => response, - Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), - } + Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>, + State(state): State, + ws: WebSocketUpgrade, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| flow_websocket::handle_ipv4_flow(socket, state, direction, flow_direction, time_type)) } -#[get("/websocket/ipv6/{direction}/{flow_direction}/{time_type}")] async fn websocket_ipv6( - req: HttpRequest, - stream: web::Payload, - path: web::Path<(Direction, FlowDirection, TimeType)>, - app_config: web::Data, - statistics: web::Data, -) -> impl Responder { - match flow_websocket::websocket_ipv6_flow(req, stream, path, app_config, statistics).await { - Ok(response) => response, - Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), - } + Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>, + State(state): State, + ws: WebSocketUpgrade, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| flow_websocket::handle_ipv6_flow(socket, state, direction, flow_direction, time_type)) } diff --git a/mantis/src/web/api/default.rs b/mantis/src/web/api/default.rs index b46f256..b0720e0 100644 --- a/mantis/src/web/api/default.rs +++ b/mantis/src/web/api/default.rs @@ -1,10 +1,10 @@ -use actix_web::{HttpRequest, HttpResponse, Responder}; -use mime_guess::from_path; +use axum::http::{StatusCode, Uri}; +use axum::response::{IntoResponse, Response}; use crate::utils::static_files::StaticFiles; -pub async fn default_route(req: HttpRequest) -> impl Responder { - let request_path = req.path(); +pub async fn default_route(uri: Uri) -> Response { + let request_path = uri.path(); let file_system_path = if request_path == "/" { "web/index.html".to_string() @@ -13,30 +13,31 @@ pub async fn default_route(req: HttpRequest) -> impl Responder { }; if let Some(content) = StaticFiles::get(&file_system_path) { - let mime_type = from_path(&file_system_path).first_or_octet_stream(); - return HttpResponse::Ok() - .content_type(mime_type.as_ref()) - .body(content.data.into_owned()); + let mime_type = mime_guess::from_path(&file_system_path).first_or_octet_stream(); + return ( + [("content-type", mime_type.as_ref().to_string())], + content.data.into_owned(), + ) + .into_response(); } let html_path = format!("{}.html", file_system_path); if let Some(content) = StaticFiles::get(&html_path) { - return HttpResponse::Ok() - .content_type("text/html") - .body(content.data.into_owned()); + return ([("content-type", "text/html")], content.data.into_owned()).into_response(); } let index_path = format!("{}/index.html", file_system_path); if let Some(content) = StaticFiles::get(&index_path) { - return HttpResponse::Ok() - .content_type("text/html") - .body(content.data.into_owned()); + return ([("content-type", "text/html")], content.data.into_owned()).into_response(); } match StaticFiles::get("web/404.html") { - Some(page) => HttpResponse::NotFound() - .content_type("text/html") - .body(page.data.into_owned()), - None => HttpResponse::NotFound().body("404 Not Found"), + Some(page) => ( + StatusCode::NOT_FOUND, + [("content-type", "text/html")], + page.data.into_owned(), + ) + .into_response(), + None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), } -} \ No newline at end of file +} diff --git a/mantis/src/web/api/detection_alert.rs b/mantis/src/web/api/detection_alert.rs index 49075e0..9f74f04 100644 --- a/mantis/src/web/api/detection_alert.rs +++ b/mantis/src/web/api/detection_alert.rs @@ -1,23 +1,15 @@ -use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::{Router, routing::get}; -use crate::core::infrastructure::detection_alert::DetectionAlert; +use crate::core::app_state::AppState; use crate::web::websocket::alert_websocket; -pub fn initialize() -> Scope { - web::scope("/detection") - .service(websocket_alert) +pub fn router() -> Router { + Router::new().route("/websocket/alert", get(websocket_alert)) } -#[get("/websocket/alert")] -async fn websocket_alert( - req: HttpRequest, - stream: web::Payload, - da: web::Data, -) -> impl Responder { - match alert_websocket::websocket_alert(req, stream, da).await { - Ok(response) => response, - Err(err) => { - HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)) - } - } +async fn websocket_alert(ws: axum::extract::ws::WebSocketUpgrade, State(state): State) -> impl IntoResponse { + let rx = state.detection_alert.subscribe(); + ws.on_upgrade(|socket| alert_websocket::handle_alert(socket, rx)) } diff --git a/mantis/src/web/api/health.rs b/mantis/src/web/api/health.rs index e4c2537..7872464 100644 --- a/mantis/src/web/api/health.rs +++ b/mantis/src/web/api/health.rs @@ -1,35 +1,30 @@ -use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::{Router, routing::get}; -use crate::core::infrastructure::health::SystemHealth; +use crate::core::app_state::AppState; use crate::web::websocket::health_websocket; -pub fn initialize() -> Scope { - web::scope("/health") - .service(get_current_metrics) - .service(get_health_status) - .service(websocket_metrics) +pub fn router() -> Router { + Router::new() + .route("/metrics", get(get_current_metrics)) + .route("/status", get(get_health_status)) + .route("/websocket/metrics", get(websocket_metrics)) } -#[get("/metrics")] -async fn get_current_metrics(health: web::Data) -> impl Responder { - let metrics = health.get_current_metrics().await; - HttpResponse::Ok().json(metrics) +async fn get_current_metrics(State(state): State) -> impl IntoResponse { + Json(state.health.get_current_metrics().await) } -#[get("/status")] -async fn get_health_status(health: web::Data) -> impl Responder { - let status = health.is_system_healthy().await; - HttpResponse::Ok().json(status) +async fn get_health_status(State(state): State) -> impl IntoResponse { + Json(state.health.is_system_healthy().await) } -#[get("/websocket/metrics")] async fn websocket_metrics( - req: HttpRequest, - stream: web::Payload, - health: web::Data, -) -> impl Responder { - match health_websocket::websocket_system_health(req, stream, health).await { - Ok(response) => response, - Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), - } -} \ No newline at end of file + ws: axum::extract::ws::WebSocketUpgrade, + State(state): State, +) -> impl IntoResponse { + let rx = state.health.subscribe_to_metrics(); + ws.on_upgrade(|socket| health_websocket::handle_health(socket, rx)) +} diff --git a/mantis/src/web/api/misc.rs b/mantis/src/web/api/misc.rs index 1682654..55f87cf 100644 --- a/mantis/src/web/api/misc.rs +++ b/mantis/src/web/api/misc.rs @@ -1,14 +1,14 @@ -use actix_web::{get, web, HttpResponse, Responder, Scope}; +use axum::Json; +use axum::response::IntoResponse; +use axum::{Router, routing::get}; +use crate::core::app_state::AppState; use crate::utils::boot_time::boot_time; -pub fn initialize() -> Scope { - web::scope("/misc") - .service(get_boot_time) +pub fn router() -> Router { + Router::new().route("/boot_time", get(get_boot_time)) } -#[get("/boot_time")] -async fn get_boot_time() -> impl Responder { - let boot_time = boot_time(); - HttpResponse::Ok().json(boot_time) +async fn get_boot_time() -> impl IntoResponse { + Json(boot_time()) } diff --git a/mantis/src/web/api/mod.rs b/mantis/src/web/api/mod.rs index 10cdba6..37af96a 100644 --- a/mantis/src/web/api/mod.rs +++ b/mantis/src/web/api/mod.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod control; pub mod default; pub mod detection_alert; diff --git a/mantis/src/web/middleware/auth.rs b/mantis/src/web/middleware/auth.rs new file mode 100644 index 0000000..39e1029 --- /dev/null +++ b/mantis/src/web/middleware/auth.rs @@ -0,0 +1,43 @@ +use axum::extract::FromRequestParts; +use axum::http::{StatusCode, request::Parts}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; +use serde::{Deserialize, Serialize}; + +use crate::core::app_state::AppState; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Claims { + pub sub: String, + pub username: String, + pub role: String, + pub exp: usize, +} + +pub struct AuthenticatedUser(pub Claims); + +impl FromRequestParts for AuthenticatedUser { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { + let auth_cfg = state + .app_config + .auth + .as_ref() + .ok_or((StatusCode::NOT_IMPLEMENTED, "Auth not configured"))?; + + let token = parts + .headers + .get("Authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.strip_prefix("Bearer ")) + .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?; + + decode::( + token, + &DecodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()), + &Validation::new(Algorithm::HS256), + ) + .map(|d| AuthenticatedUser(d.claims)) + .map_err(|_| (StatusCode::UNAUTHORIZED, "Unauthorized")) + } +} diff --git a/mantis/src/web/middleware/mod.rs b/mantis/src/web/middleware/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/mantis/src/web/middleware/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/mantis/src/web/mod.rs b/mantis/src/web/mod.rs index a3766ff..764dc0b 100644 --- a/mantis/src/web/mod.rs +++ b/mantis/src/web/mod.rs @@ -1,2 +1,3 @@ pub mod api; +pub mod middleware; pub mod websocket; diff --git a/mantis/src/web/websocket/alert_websocket.rs b/mantis/src/web/websocket/alert_websocket.rs index d029fcb..074f0e4 100644 --- a/mantis/src/web/websocket/alert_websocket.rs +++ b/mantis/src/web/websocket/alert_websocket.rs @@ -1,91 +1,47 @@ -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use actix_ws::{handle, Message, MessageStream, Session}; -use futures_util::StreamExt; +use axum::extract::ws::{Message, WebSocket}; +use futures_util::{SinkExt, StreamExt}; use macros::log; use tokio::sync::broadcast; -use crate::core::infrastructure::detection_alert::DetectionAlert; -use crate::model::ml_detection::UnifiedAlert; use crate::model::error::http::HttpError; use crate::model::error::misc::MiscError; use crate::model::log::http::HttpLog; +use crate::model::ml_detection::UnifiedAlert; -pub async fn websocket_alert( - req: HttpRequest, - body: web::Payload, - da: web::Data, -) -> Result { - let (response, session, msg_stream) = handle(&req, body)?; +pub async fn handle_alert(socket: WebSocket, mut broadcast_rx: broadcast::Receiver) { + let (mut sender, mut receiver) = socket.split(); - let broadcast_rx = da.subscribe(); - - actix_web::rt::spawn(async move { - handle_alert_connection(session, msg_stream, broadcast_rx).await; - }); - - Ok(response) -} - -async fn handle_alert_connection( - mut session: Session, - mut msg_stream: MessageStream, - mut broadcast_rx: broadcast::Receiver, -) { loop { tokio::select! { - msg_result = msg_stream.next() => { - if !handle_client_message(&mut session, msg_result).await { - break; - } - }, - broadcast_result = broadcast_rx.recv() => { - match broadcast_result { - Ok(alert) => { - if !send_alert(&mut session, &alert).await { - break; - } + msg = receiver.next() => { + match msg { + Some(Ok(Message::Ping(data))) => { + if sender.send(Message::Pong(data)).await.is_err() { break; } } - Err(broadcast::error::RecvError::Lagged(skipped)) => { - log!(HttpLog::WebSocketLaged(skipped)); - continue; - } - Err(broadcast::error::RecvError::Closed) => { + Some(Ok(Message::Close(_))) | None => break, + Some(Err(e)) => { + log!(HttpError::WebSocketError { msg: e.to_string() }); break; } + _ => {} } - }, - } - } - - let _ = session.close(None).await; -} - -async fn handle_client_message( - session: &mut Session, - msg_result: Option>, -) -> bool { - match msg_result { - Some(Ok(Message::Text(_))) => true, - Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), - Some(Ok(Message::Close(reason))) => { - let _ = (session.clone()).close(reason).await; - false - } - Some(Err(err)) => { - log!(HttpError::WebSocketError(err)); - false - } - None => false, - _ => true, - } -} - -async fn send_alert(session: &mut Session, alert: &UnifiedAlert) -> bool { - match serde_json::to_string(alert) { - Ok(json) => session.text(json).await.is_ok(), - Err(err) => { - log!(MiscError::SerializeError(err)); - false + } + result = broadcast_rx.recv() => { + match result { + Ok(alert) => { + match serde_json::to_string(&alert) { + Ok(json) => { + if sender.send(Message::Text(json.into())).await.is_err() { break; } + } + Err(e) => { log!(MiscError::SerializeError(e)); } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + log!(HttpLog::WebSocketLaged { skipped: n }); + } + Err(broadcast::error::RecvError::Closed) => break, + } + } } } } diff --git a/mantis/src/web/websocket/flow_websocket.rs b/mantis/src/web/websocket/flow_websocket.rs index 1298b7c..a29bf2e 100644 --- a/mantis/src/web/websocket/flow_websocket.rs +++ b/mantis/src/web/websocket/flow_websocket.rs @@ -1,192 +1,96 @@ -use std::sync::Arc; - -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use actix_ws::{handle, Message, MessageStream, Session}; -use futures_util::StreamExt; +use axum::extract::ws::{Message, WebSocket}; +use futures_util::{SinkExt, StreamExt}; use macros::log; -use tokio::time::{interval, Duration}; +use tokio::time::{Duration, interval}; -use crate::core::ebpf::statistics::Statistics; -use crate::core::infrastructure::app_config::AppConfig; +use crate::core::app_state::AppState; use crate::model::direction::{Direction, FlowDirection}; use crate::model::error::http::HttpError; use crate::model::error::misc::MiscError; use crate::model::time_type::TimeType; -pub async fn websocket_ipv4_flow( - req: HttpRequest, - body: web::Payload, - path: web::Path<(Direction, FlowDirection, TimeType)>, - app_config: web::Data, - statistics: web::Data, -) -> Result { - let app_config = app_config.into_inner(); - let statistics = statistics.into_inner(); - let (direction, flow_direction, time_type) = path.into_inner(); - let (response, session, msg_stream) = handle(&req, body)?; - - actix_web::rt::spawn(async move { - handle_ipv4_flow_connection( - app_config, - statistics, - session, - msg_stream, - direction, - flow_direction, - time_type, - ) - .await; - }); - - Ok(response) -} - -pub async fn websocket_ipv6_flow( - req: HttpRequest, - body: web::Payload, - path: web::Path<(Direction, FlowDirection, TimeType)>, - app_config: web::Data, - statistics: web::Data, -) -> Result { - let app_config = app_config.into_inner(); - let statistics = statistics.into_inner(); - let (direction, flow_direction, time_type) = path.into_inner(); - let (response, session, msg_stream) = handle(&req, body)?; - - actix_web::rt::spawn(async move { - handle_ipv6_flow_connection( - app_config, - statistics, - session, - msg_stream, - direction, - flow_direction, - time_type, - ) - .await; - }); - - Ok(response) -} - -async fn handle_ipv4_flow_connection( - app_config: Arc, - statistics: Arc, - mut session: Session, - mut msg_stream: MessageStream, +pub async fn handle_ipv4_flow( + socket: WebSocket, + state: AppState, direction: Direction, flow_direction: FlowDirection, time_type: TimeType, ) { - let config = app_config.config.clone(); - let refresh_interval = Duration::from_secs(config.refresh_interval); + let refresh_interval = Duration::from_secs(state.app_config.refresh_interval); + let (mut sender, mut receiver) = socket.split(); let mut data_interval = interval(refresh_interval); loop { tokio::select! { - msg_result = msg_stream.next() => { - if !handle_client_message(&mut session, msg_result).await { - break; + msg = receiver.next() => { + match msg { + Some(Ok(Message::Ping(data))) => { + if sender.send(Message::Pong(data)).await.is_err() { break; } + } + Some(Ok(Message::Close(_))) | None => break, + Some(Err(e)) => { + log!(HttpError::WebSocketError { msg: e.to_string() }); + break; + } + _ => {} } - }, + } _ = data_interval.tick() => { - if !send_ipv4_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { - break; + match state.statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await { + Ok(data) => { + match serde_json::to_string(&data) { + Ok(json) => { + if sender.send(Message::Text(json.into())).await.is_err() { break; } + } + Err(e) => { log!(MiscError::SerializeError(e)); } + } + } + Err(e) => { log!(e); break; } } - }, + } } } - - let _ = session.close(None).await; } -async fn handle_ipv6_flow_connection( - app_config: Arc, - statistics: Arc, - mut session: Session, - mut msg_stream: MessageStream, +pub async fn handle_ipv6_flow( + socket: WebSocket, + state: AppState, direction: Direction, flow_direction: FlowDirection, time_type: TimeType, ) { - let config = app_config.config.clone(); - let refresh_interval = Duration::from_secs(config.refresh_interval); + let refresh_interval = Duration::from_secs(state.app_config.refresh_interval); + let (mut sender, mut receiver) = socket.split(); let mut data_interval = interval(refresh_interval); loop { tokio::select! { - msg_result = msg_stream.next() => { - if !handle_client_message(&mut session, msg_result).await { - break; + msg = receiver.next() => { + match msg { + Some(Ok(Message::Ping(data))) => { + if sender.send(Message::Pong(data)).await.is_err() { break; } + } + Some(Ok(Message::Close(_))) | None => break, + Some(Err(e)) => { + log!(HttpError::WebSocketError { msg: e.to_string() }); + break; + } + _ => {} } - }, + } _ = data_interval.tick() => { - if !send_ipv6_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { - break; + match state.statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await { + Ok(data) => { + match serde_json::to_string(&data) { + Ok(json) => { + if sender.send(Message::Text(json.into())).await.is_err() { break; } + } + Err(e) => { log!(MiscError::SerializeError(e)); } + } + } + Err(e) => { log!(e); break; } } - }, - } - } - - let _ = session.close(None).await; -} - -async fn handle_client_message( - session: &mut Session, - msg_result: Option>, -) -> bool { - match msg_result { - Some(Ok(Message::Text(_))) => true, - Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), - Some(Ok(Message::Close(reason))) => { - let _ = (session.clone()).close(reason).await; - false - } - Some(Err(err)) => { - log!(HttpError::WebSocketError(err)); - false - } - None => false, - _ => true, - } -} - -async fn send_ipv4_flow_data( - statistics: &Arc, - session: &mut Session, - direction: Direction, - flow_direction: FlowDirection, - time_type: TimeType, -) -> bool { - let flow_data = match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await { - Ok(data) => data, - Err(e) => { log!(e); return false; } - }; - match serde_json::to_string(&flow_data) { - Ok(json) => session.text(json).await.is_ok(), - Err(err) => { - log!(MiscError::SerializeError(err)); - true - } - } -} - -async fn send_ipv6_flow_data( - statistics: &Arc, - session: &mut Session, - direction: Direction, - flow_direction: FlowDirection, - time_type: TimeType, -) -> bool { - let flow_data = match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await { - Ok(data) => data, - Err(e) => { log!(e); return false; } - }; - match serde_json::to_string(&flow_data) { - Ok(json) => session.text(json).await.is_ok(), - Err(err) => { - log!(MiscError::SerializeError(err)); - true + } } } } diff --git a/mantis/src/web/websocket/health_websocket.rs b/mantis/src/web/websocket/health_websocket.rs index 7997571..a93f474 100644 --- a/mantis/src/web/websocket/health_websocket.rs +++ b/mantis/src/web/websocket/health_websocket.rs @@ -1,91 +1,47 @@ -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use actix_ws::{handle, Message, MessageStream, Session}; -use futures_util::StreamExt; +use axum::extract::ws::{Message, WebSocket}; +use futures_util::{SinkExt, StreamExt}; use macros::log; use tokio::sync::broadcast; -use crate::core::infrastructure::health::SystemHealth; use crate::model::error::http::HttpError; use crate::model::error::misc::MiscError; -use crate::model::log::http::HttpLog; use crate::model::health::SystemHealthMetrics; +use crate::model::log::http::HttpLog; -pub async fn websocket_system_health( - req: HttpRequest, - body: web::Payload, - health: web::Data, -) -> Result { - let (response, session, msg_stream) = handle(&req, body)?; +pub async fn handle_health(socket: WebSocket, mut broadcast_rx: broadcast::Receiver) { + let (mut sender, mut receiver) = socket.split(); - let broadcast_rx = health.subscribe_to_metrics(); - - actix_web::rt::spawn(async move { - handle_health_connection(session, msg_stream, broadcast_rx).await; - }); - - Ok(response) -} - -async fn handle_health_connection( - mut session: Session, - mut msg_stream: MessageStream, - mut broadcast_rx: broadcast::Receiver, -) { loop { tokio::select! { - msg_result = msg_stream.next() => { - if !handle_client_message(&mut session, msg_result).await { - break; - } - }, - broadcast_result = broadcast_rx.recv() => { - match broadcast_result { - Ok(metrics) => { - if !send_metrics(&mut session, &metrics).await { - break; - } + msg = receiver.next() => { + match msg { + Some(Ok(Message::Ping(data))) => { + if sender.send(Message::Pong(data)).await.is_err() { break; } } - Err(broadcast::error::RecvError::Lagged(skipped)) => { - log!(HttpLog::WebSocketLaged(skipped)); - continue; - } - Err(broadcast::error::RecvError::Closed) => { + Some(Ok(Message::Close(_))) | None => break, + Some(Err(e)) => { + log!(HttpError::WebSocketError { msg: e.to_string() }); break; } + _ => {} } - }, + } + result = broadcast_rx.recv() => { + match result { + Ok(metrics) => { + match serde_json::to_string(&metrics) { + Ok(json) => { + if sender.send(Message::Text(json.into())).await.is_err() { break; } + } + Err(e) => { log!(MiscError::SerializeError(e)); } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + log!(HttpLog::WebSocketLaged { skipped: n }); + } + Err(broadcast::error::RecvError::Closed) => break, + } + } } } - - let _ = session.close(None).await; -} - -async fn handle_client_message( - session: &mut Session, - msg_result: Option>, -) -> bool { - match msg_result { - Some(Ok(Message::Text(_))) => true, - Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), - Some(Ok(Message::Close(reason))) => { - let _ = (session.clone()).close(reason).await; - false - } - Some(Err(err)) => { - log!(HttpError::WebSocketError(err)); - false - } - None => false, - _ => true, - } } - -async fn send_metrics(session: &mut Session, metrics: &SystemHealthMetrics) -> bool { - match serde_json::to_string(metrics) { - Ok(json) => session.text(json).await.is_ok(), - Err(err) => { - log!(MiscError::SerializeError(err)); - false - } - } -} \ No newline at end of file diff --git a/mantis/src/web/websocket/mod.rs b/mantis/src/web/websocket/mod.rs index e8139e1..8ae1755 100644 --- a/mantis/src/web/websocket/mod.rs +++ b/mantis/src/web/websocket/mod.rs @@ -1,3 +1,3 @@ +pub mod alert_websocket; pub mod flow_websocket; pub mod health_websocket; -pub mod alert_websocket;